]> git.proxmox.com Git - mirror_novnc.git/blobdiff - app/ui.js
Merge pull request #996 from PeterDaveHelloKitchen/zh-tw-translation
[mirror_novnc.git] / app / ui.js
index ad80c63baceb24e856e9276c63ec4abf62fc25c2..0991c708ea41006880912a86a0d5ff63a086886f 100644 (file)
--- a/app/ui.js
+++ b/app/ui.js
 /* jslint white: false, browser: true */
 /* global window, document.getElementById, Util, WebUtil, RFB, Display */
 
-/* [module]
- * import Util from "../core/util";
- * import KeyTable from "../core/input/keysym";
- * import keysyms from "./keysymdef";
- * import RFB from "../core/rfb";
- * import Display from "../core/display";
- * import WebUtil from "./webutil";
- */
+import * as Log from '../core/util/logging.js';
+import _, { l10n } from './localization.js';
+import { isTouchDevice } from '../core/util/browsers.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 Display from "../core/display.js";
+import * as WebUtil from "./webutil.js";
+
+var UI = {
+
+    connected: false,
+    desktopName: "",
+
+    statusTimeout: null,
+    hideKeyboardTimeout: null,
+    idleControlbarTimeout: null,
+    closeControlbarTimeout: null,
+
+    controlbarGrabbed: false,
+    controlbarDrag: false,
+    controlbarMouseDownClientY: 0,
+    controlbarMouseDownOffsetY: 0,
+
+    isSafari: false,
+    lastKeyboardinput: null,
+    defaultKeyboardinputLen: 100,
+
+    inhibit_reconnect: true,
+    reconnect_callback: null,
+    reconnect_password: null,
+
+    prime: function(callback) {
+        if (document.readyState === "interactive" || document.readyState === "complete") {
+            UI.load(callback);
+        } else {
+            document.addEventListener('DOMContentLoaded', UI.load.bind(UI, callback));
+        }
+    },
 
-var UI;
+    // Setup rfb object, load settings from browser storage, then call
+    // UI.init to setup the UI/menus
+    load: function(callback) {
+        WebUtil.initSettings(UI.start, callback);
+    },
 
-(function () {
-    "use strict";
+    // Render default UI and initialize settings menu
+    start: function(callback) {
 
-    // Fallback for all uncought errors
-    window.addEventListener('error', function(event) {
-        try {
-            var msg, div, text;
+        // Setup global variables first
+        UI.isSafari = (navigator.userAgent.indexOf('Safari') !== -1 &&
+                       navigator.userAgent.indexOf('Chrome') === -1);
 
-            msg = document.getElementById('noVNC_fallback_errormsg');
+        UI.initSettings();
 
-            // Only show the initial error
-            if (msg.hasChildNodes()) {
-                return false;
-            }
+        // Translate the DOM
+        l10n.translateDOM();
 
-            div = document.createElement("div");
-            div.appendChild(document.createTextNode(event.message));
-            msg.appendChild(div);
-
-            div = document.createElement("div");
-            div.className = 'noVNC_location';
-            text = event.filename + ":" + event.lineno + ":" + event.colno;
-            div.appendChild(document.createTextNode(text));
-            msg.appendChild(div);
-
-            if ((event.error !== undefined) &&
-                (event.error.stack !== undefined)) {
-                div = document.createElement("div");
-                div.className = 'noVNC_stack';
-                div.appendChild(document.createTextNode(event.error.stack));
-                msg.appendChild(div);
-            }
+        // Adapt the interface for touch screen devices
+        if (isTouchDevice) {
+            document.documentElement.classList.add("noVNC_touch");
+            // Remove the address bar
+            setTimeout(function() { window.scrollTo(0, 1); }, 100);
+        }
 
-            document.getElementById('noVNC_fallback_error')
-                .classList.add("noVNC_open");
-        } catch (exc) {
-            document.write("noVNC encountered an error.");
+        // Restore control bar position
+        if (WebUtil.readSetting('controlbar_pos') === 'right') {
+            UI.toggleControlbarSide();
         }
-        // Don't return true since this would prevent the error
-        // from being printed to the browser console.
-        return false;
-    });
 
-    // Set up translations
-    var LINGUAS = ["de", "el", "nl", "sv"];
-    Util.Localisation.setup(LINGUAS);
-    if (Util.Localisation.language !== "en") {
-        WebUtil.load_scripts(
-            {'app': ["locale/" + Util.Localisation.language + ".js"]});
-    }
-
-    /* [begin skip-as-module] */
-    // Load supporting scripts
-    WebUtil.load_scripts(
-        {'core': ["base64.js", "websock.js", "des.js", "input/keysymdef.js",
-                  "input/xtscancodes.js", "input/util.js", "input/devices.js",
-                  "display.js", "inflator.js", "rfb.js", "input/keysym.js"]});
-
-    window.onscriptsload = function () { UI.load(); };
-    /* [end skip-as-module] */
-
-    var _ = Util.Localisation.get;
-
-    UI = {
-
-        connected: false,
-        desktopName: "",
-
-        resizeTimeout: null,
-        statusTimeout: null,
-        hideKeyboardTimeout: null,
-        idleControlbarTimeout: null,
-        closeControlbarTimeout: null,
-
-        controlbarGrabbed: false,
-        controlbarDrag: false,
-        controlbarMouseDownClientY: 0,
-        controlbarMouseDownOffsetY: 0,
-
-        isSafari: false,
-        rememberedClipSetting: null,
-        lastKeyboardinput: null,
-        defaultKeyboardinputLen: 100,
-
-        inhibit_reconnect: true,
-        reconnect_callback: null,
-        reconnect_password: null,
-
-        // Setup rfb object, load settings from browser storage, then call
-        // UI.init to setup the UI/menus
-        load: function(callback) {
-            WebUtil.initSettings(UI.start, callback);
-        },
-
-        // Render default UI and initialize settings menu
-        start: function(callback) {
-
-            // Setup global variables first
-            UI.isSafari = (navigator.userAgent.indexOf('Safari') !== -1 &&
-                           navigator.userAgent.indexOf('Chrome') === -1);
-
-            UI.initSettings();
-
-            // Translate the DOM
-            Util.Localisation.translateDOM();
-
-            // Adapt the interface for touch screen devices
-            if (Util.isTouchDevice) {
-                document.documentElement.classList.add("noVNC_touch");
-                // Remove the address bar
-                setTimeout(function() { window.scrollTo(0, 1); }, 100);
-            }
+        UI.initFullscreen();
 
-            // Restore control bar position
-            if (WebUtil.readSetting('controlbar_pos') === 'right') {
-                UI.toggleControlbarSide();
-            }
+        // 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);
 
-            UI.initFullscreen();
+        // Bootstrap fallback input handler
+        UI.keyboardinputReset();
 
-            // Setup event handlers
-            UI.addResizeHandlers();
-            UI.addControlbarHandlers();
-            UI.addTouchSpecificHandlers();
-            UI.addExtraKeysHandlers();
-            UI.addXvpHandlers();
-            UI.addConnectionControlHandlers();
-            UI.addClipboardHandlers();
-            UI.addSettingsHandlers();
-            document.getElementById("noVNC_status")
-                .addEventListener('click', UI.hideStatus);
+        UI.openControlbar();
 
-            UI.openControlbar();
+        UI.updateVisualState('init');
+
+        document.documentElement.classList.remove("noVNC_loading");
 
+        var 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
-            if (!autoconnect) {
-                UI.openConnectPanel();
-            }
+            UI.openConnectPanel();
+        }
 
-            UI.updateViewClip();
+        if (typeof callback === "function") {
+            callback(UI.rfb);
+        }
+    },
+
+    initFullscreen: function() {
+        // Only show the button if fullscreen is properly supported
+        // * Safari doesn't support alphanumerical input while in fullscreen
+        if (!UI.isSafari &&
+            (document.documentElement.requestFullscreen ||
+             document.documentElement.mozRequestFullScreen ||
+             document.documentElement.webkitRequestFullscreen ||
+             document.body.msRequestFullscreen)) {
+            document.getElementById('noVNC_fullscreen_button')
+                .classList.remove("noVNC_hidden");
+            UI.addFullscreenHandlers();
+        }
+    },
 
-            UI.updateVisualState();
+    initSettings: function() {
+        var i;
 
-            document.getElementById('noVNC_setting_host').focus();
+        // Logging selection dropdown
+        var llevels = ['error', 'warn', 'info', 'debug'];
+        for (i = 0; i < llevels.length; i += 1) {
+            UI.addOption(document.getElementById('noVNC_setting_logging'),llevels[i], llevels[i]);
+        }
 
-            var autoconnect = WebUtil.getConfigVar('autoconnect', false);
-            if (autoconnect === 'true' || autoconnect == '1') {
-                autoconnect = true;
-                UI.connect();
-            } else {
-                autoconnect = false;
-            }
+        // Settings with immediate effects
+        UI.initSetting('logging', 'warn');
+        UI.updateLogging();
 
-            if (typeof callback === "function") {
-                callback(UI.rfb);
+        // if port == 80 (or 443) then it won't be present and should be
+        // set manually
+        var port = window.location.port;
+        if (!port) {
+            if (window.location.protocol.substring(0,5) == 'https') {
+                port = 443;
             }
-        },
-
-        initFullscreen: function() {
-            // Only show the button if fullscreen is properly supported
-            // * Safari doesn't support alphanumerical input while in fullscreen
-            if (!UI.isSafari &&
-                (document.documentElement.requestFullscreen ||
-                 document.documentElement.mozRequestFullScreen ||
-                 document.documentElement.webkitRequestFullscreen ||
-                 document.body.msRequestFullscreen)) {
-                document.getElementById('noVNC_fullscreen_button')
-                    .classList.remove("noVNC_hidden");
-                UI.addFullscreenHandlers();
+            else if (window.location.protocol.substring(0,4) == 'http') {
+                port = 80;
             }
-        },
-
-        initSettings: function() {
-            var i;
-
-            // Logging selection dropdown
-            var llevels = ['error', 'warn', 'info', 'debug'];
-            for (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();
+        }
 
-            // if port == 80 (or 443) then it won't be present and should be
-            // set manually
-            var port = window.location.port;
-            if (!port) {
-                if (window.location.protocol.substring(0,5) == 'https') {
-                    port = 443;
-                }
-                else if (window.location.protocol.substring(0,4) == 'http') {
-                    port = 80;
+        /* Populate the controls if defaults are provided in the URL */
+        UI.initSetting('host', window.location.hostname);
+        UI.initSetting('port', port);
+        UI.initSetting('encrypt', (window.location.protocol === "https:"));
+        UI.initSetting('view_clip', false);
+        UI.initSetting('resize', 'off');
+        UI.initSetting('shared', true);
+        UI.initSetting('view_only', 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: function() {
+        var labels = document.getElementsByTagName('LABEL');
+        for (var i = 0; i < labels.length; i++) {
+            var htmlFor = labels[i].htmlFor;
+            if (htmlFor != '') {
+                var elem = document.getElementById(htmlFor);
+                if (elem) elem.label = labels[i];
+            } else {
+                // If 'for' isn't set, use the first input element child
+                var children = labels[i].children;
+                for (var j = 0; j < children.length; j++) {
+                    if (children[j].form !== undefined) {
+                        children[j].label = labels[i];
+                        break;
+                    }
                 }
             }
-
-            /* Populate the controls if defaults are provided in the URL */
-            UI.initSetting('host', window.location.hostname);
-            UI.initSetting('port', port);
-            UI.initSetting('encrypt', (window.location.protocol === "https:"));
-            UI.initSetting('true_color', true);
-            UI.initSetting('cursor', !Util.isTouchDevice);
-            UI.initSetting('clip', false);
-            UI.initSetting('resize', 'off');
-            UI.initSetting('shared', true);
-            UI.initSetting('view_only', false);
-            UI.initSetting('path', 'websockify');
-            UI.initSetting('repeaterID', '');
-            UI.initSetting('reconnect', false);
-            UI.initSetting('reconnect_delay', 5000);
-        },
-
-        initRFB: function() {
-            try {
-                UI.rfb = new RFB({'target': document.getElementById('noVNC_canvas'),
-                                  'onNotification': UI.notification,
-                                  'onUpdateState': UI.updateState,
-                                  'onDisconnected': UI.disconnectFinished,
-                                  'onPasswordRequired': UI.passwordRequired,
-                                  'onXvpInit': UI.updateXvpButton,
-                                  'onClipboard': UI.clipboardReceive,
-                                  'onBell': UI.bell,
-                                  'onFBUComplete': UI.initialResize,
-                                  'onFBResize': UI.updateSessionSize,
-                                  'onDesktopName': UI.updateDesktopName});
-                return true;
-            } catch (exc) {
-                var msg = "Unable to create RFB client -- " + exc;
-                Util.Error(msg);
-                UI.showStatus(msg, 'error');
-                return false;
-            }
-        },
+        }
+    },
 
 /* ------^-------
- *     /INIT
- * ==============
- * EVENT HANDLERS
- * ------v------*/
-
-        addResizeHandlers: function() {
-            window.addEventListener('resize', UI.applyResizeMode);
-            window.addEventListener('resize', UI.updateViewClip);
-        },
-
-        addControlbarHandlers: function() {
-            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('keypress', UI.activateControlbar);
-
-            document.getElementById("noVNC_control_bar")
-                .addEventListener('mousedown', UI.keepControlbar);
-            document.getElementById("noVNC_control_bar")
-                .addEventListener('keypress', 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);
-
-            var exps = document.getElementsByClassName("noVNC_expander");
-            for (var i = 0;i < exps.length;i++) {
-                exps[i].addEventListener('click', UI.toggleExpander);
-            }
-        },
-
-        addTouchSpecificHandlers: function() {
-            document.getElementById("noVNC_mouse_button0")
-                .addEventListener('click', function () { UI.setMouseButton(1); });
-            document.getElementById("noVNC_mouse_button1")
-                .addEventListener('click', function () { UI.setMouseButton(2); });
-            document.getElementById("noVNC_mouse_button2")
-                .addEventListener('click', function () { UI.setMouseButton(4); });
-            document.getElementById("noVNC_mouse_button4")
-                .addEventListener('click', function () { UI.setMouseButton(0); });
-            document.getElementById("noVNC_keyboard_button")
-                .addEventListener('click', UI.toggleVirtualKeyboard);
-
-            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', function () { return 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);
-
-            window.addEventListener('load', UI.keyboardinputReset);
-        },
-
-        addExtraKeysHandlers: function() {
-            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_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);
-        },
-
-        addXvpHandlers: function() {
-            document.getElementById("noVNC_xvp_shutdown_button")
-                .addEventListener('click', function() { UI.rfb.xvpShutdown(); });
-            document.getElementById("noVNC_xvp_reboot_button")
-                .addEventListener('click', function() { UI.rfb.xvpReboot(); });
-            document.getElementById("noVNC_xvp_reset_button")
-                .addEventListener('click', function() { UI.rfb.xvpReset(); });
-            document.getElementById("noVNC_xvp_button")
-                .addEventListener('click', UI.toggleXvpPanel);
-        },
-
-        addConnectionControlHandlers: function() {
-            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_password_button")
-                .addEventListener('click', UI.setPassword);
-        },
-
-        addClipboardHandlers: function() {
-            document.getElementById("noVNC_clipboard_button")
-                .addEventListener('click', UI.toggleClipboardPanel);
-            document.getElementById("noVNC_clipboard_text")
-                .addEventListener('focus', UI.displayBlur);
-            document.getElementById("noVNC_clipboard_text")
-                .addEventListener('blur', UI.displayFocus);
-            document.getElementById("noVNC_clipboard_text")
-                .addEventListener('change', UI.clipboardSend);
-            document.getElementById("noVNC_clipboard_clear_button")
-                .addEventListener('click', UI.clipboardClear);
-        },
-
-        // Add a call to save settings when the element changes,
-        // unless the optional parameter changeFunc is used instead.
-        addSettingChangeHandler: function(name, changeFunc) {
-            var settingElem = document.getElementById("noVNC_setting_" + name);
-            if (changeFunc === undefined) {
-                changeFunc = function () { UI.saveSetting(name); };
-            }
-            settingElem.addEventListener('change', changeFunc);
-        },
-
-        addSettingsHandlers: function() {
-            document.getElementById("noVNC_settings_button")
-                .addEventListener('click', UI.toggleSettingsPanel);
-
-            UI.addSettingChangeHandler('encrypt');
-            UI.addSettingChangeHandler('true_color');
-            UI.addSettingChangeHandler('cursor');
-            UI.addSettingChangeHandler('resize');
-            UI.addSettingChangeHandler('resize', UI.enableDisableViewClip);
-            UI.addSettingChangeHandler('resize', UI.applyResizeMode);
-            UI.addSettingChangeHandler('clip');
-            UI.addSettingChangeHandler('shared');
-            UI.addSettingChangeHandler('view_only');
-            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: function() {
-            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);
-        },
+*     /INIT
+* ==============
+* EVENT HANDLERS
+* ------v------*/
+
+    addControlbarHandlers: function() {
+        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);
+
+        var exps = document.getElementsByClassName("noVNC_expander");
+        for (var i = 0;i < exps.length;i++) {
+            exps[i].addEventListener('click', UI.toggleExpander);
+        }
+    },
+
+    addTouchSpecificHandlers: function() {
+        document.getElementById("noVNC_mouse_button0")
+            .addEventListener('click', function () { UI.setMouseButton(1); });
+        document.getElementById("noVNC_mouse_button1")
+            .addEventListener('click', function () { UI.setMouseButton(2); });
+        document.getElementById("noVNC_mouse_button2")
+            .addEventListener('click', function () { UI.setMouseButton(4); });
+        document.getElementById("noVNC_mouse_button4")
+            .addEventListener('click', function () { UI.setMouseButton(0); });
+        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', function () { return 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: function() {
+        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_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: function() {
+        document.getElementById("noVNC_shutdown_button")
+            .addEventListener('click', function() { UI.rfb.machineShutdown(); });
+        document.getElementById("noVNC_reboot_button")
+            .addEventListener('click', function() { UI.rfb.machineReboot(); });
+        document.getElementById("noVNC_reset_button")
+            .addEventListener('click', function() { UI.rfb.machineReset(); });
+        document.getElementById("noVNC_power_button")
+            .addEventListener('click', UI.togglePowerPanel);
+    },
+
+    addConnectionControlHandlers: function() {
+        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_password_button")
+            .addEventListener('click', UI.setPassword);
+    },
+
+    addClipboardHandlers: function() {
+        document.getElementById("noVNC_clipboard_button")
+            .addEventListener('click', UI.toggleClipboardPanel);
+        document.getElementById("noVNC_clipboard_text")
+            .addEventListener('change', UI.clipboardSend);
+        document.getElementById("noVNC_clipboard_clear_button")
+            .addEventListener('click', UI.clipboardClear);
+    },
+
+    // Add a call to save settings when the element changes,
+    // unless the optional parameter changeFunc is used instead.
+    addSettingChangeHandler: function(name, changeFunc) {
+        var settingElem = document.getElementById("noVNC_setting_" + name);
+        if (changeFunc === undefined) {
+            changeFunc = function () { UI.saveSetting(name); };
+        }
+        settingElem.addEventListener('change', changeFunc);
+    },
+
+    addSettingsHandlers: function() {
+        document.getElementById("noVNC_settings_button")
+            .addEventListener('click', UI.toggleSettingsPanel);
+
+        UI.addSettingChangeHandler('encrypt');
+        UI.addSettingChangeHandler('resize');
+        UI.addSettingChangeHandler('resize', UI.enableDisableViewClip);
+        UI.addSettingChangeHandler('resize', UI.applyResizeMode);
+        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('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: function() {
+        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
@@ -449,337 +379,350 @@ var UI;
  *     VISUAL
  * ------v------*/
 
-        updateState: function(rfb, state, oldstate) {
-            var msg;
+    // Disable/enable controls depending on connection state
+    updateVisualState: function(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");
+
+        let transition_elem = document.getElementById("noVNC_transition_text");
+        switch (state) {
+            case 'init':
+                break;
+            case 'connecting':
+                transition_elem.textContent = _("Connecting...");
+                document.documentElement.classList.add("noVNC_connecting");
+                break;
+            case 'connected':
+                document.documentElement.classList.add("noVNC_connected");
+                break;
+            case 'disconnecting':
+                transition_elem.textContent = _("Disconnecting...");
+                document.documentElement.classList.add("noVNC_disconnecting");
+                break;
+            case 'disconnected':
+                break;
+            case 'reconnecting':
+                transition_elem.textContent = _("Reconnecting...");
+                document.documentElement.classList.add("noVNC_reconnecting");
+                break;
+            default:
+                Log.Error("Invalid visual state: " + state);
+                UI.showStatus(_("Internal error"), 'error');
+                return;
+        }
 
-            document.documentElement.classList.remove("noVNC_connecting");
-            document.documentElement.classList.remove("noVNC_connected");
-            document.documentElement.classList.remove("noVNC_disconnecting");
-            document.documentElement.classList.remove("noVNC_reconnecting");
+        UI.enableDisableViewClip();
+
+        if (UI.connected) {
+            UI.disableSetting('encrypt');
+            UI.disableSetting('shared');
+            UI.disableSetting('host');
+            UI.disableSetting('port');
+            UI.disableSetting('path');
+            UI.disableSetting('repeaterID');
+            UI.setMouseButton(1);
+
+            // 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();
+        }
 
-            switch (state) {
-                case 'connecting':
-                    document.getElementById("noVNC_transition_text").textContent = _("Connecting...");
-                    document.documentElement.classList.add("noVNC_connecting");
-                    break;
-                case 'connected':
-                    UI.connected = true;
-                    UI.inhibit_reconnect = false;
-                    document.documentElement.classList.add("noVNC_connected");
-                    if (rfb && rfb.get_encrypt()) {
-                        msg = _("Connected (encrypted) to ") + UI.desktopName;
-                    } else {
-                        msg = _("Connected (unencrypted) to ") + UI.desktopName;
-                    }
-                    UI.showStatus(msg);
-                    break;
-                case 'disconnecting':
-                    UI.connected = false;
-                    document.getElementById("noVNC_transition_text").textContent = _("Disconnecting...");
-                    document.documentElement.classList.add("noVNC_disconnecting");
-                    break;
-                case 'disconnected':
-                    UI.showStatus(_("Disconnected"));
-                    break;
-                default:
-                    msg = "Invalid UI state";
-                    Util.Error(msg);
-                    UI.showStatus(msg, 'error');
-                    break;
-            }
+        // Hide input related buttons in view only mode
+        if (UI.rfb && UI.rfb.viewOnly) {
+            document.getElementById('noVNC_keyboard_button')
+                .classList.add('noVNC_hidden');
+            document.getElementById('noVNC_toggle_extra_keys_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');
+        }
 
-            UI.updateVisualState();
-        },
+        // State change disables viewport dragging.
+        // It is enabled (toggled) by direct click on the button
+        UI.setViewDrag(false);
 
-        // Disable/enable controls depending on connection state
-        updateVisualState: function() {
-            //Util.Debug(">> updateVisualState");
-            document.getElementById('noVNC_setting_encrypt').disabled = UI.connected;
-            document.getElementById('noVNC_setting_true_color').disabled = UI.connected;
-            if (Util.browserSupportsCursorURIs()) {
-                document.getElementById('noVNC_setting_cursor').disabled = UI.connected;
-            } else {
-                UI.updateSetting('cursor', !Util.isTouchDevice);
-                document.getElementById('noVNC_setting_cursor').disabled = true;
-            }
+        // State change also closes the password dialog
+        document.getElementById('noVNC_password_dlg')
+            .classList.remove('noVNC_open');
+    },
 
-            UI.enableDisableViewClip();
-            document.getElementById('noVNC_setting_shared').disabled = UI.connected;
-            document.getElementById('noVNC_setting_view_only').disabled = UI.connected;
-            document.getElementById('noVNC_setting_host').disabled = UI.connected;
-            document.getElementById('noVNC_setting_port').disabled = UI.connected;
-            document.getElementById('noVNC_setting_path').disabled = UI.connected;
-            document.getElementById('noVNC_setting_repeaterID').disabled = UI.connected;
-            document.getElementById('noVNC_setting_reconnect').disabled = UI.connected;
-            document.getElementById('noVNC_setting_reconnect_delay').disabled = UI.connected;
-
-            if (UI.connected) {
-                UI.updateViewClip();
-                UI.setMouseButton(1);
-
-                // Hide the controlbar after 2 seconds
-                UI.closeControlbarTimeout = setTimeout(UI.closeControlbar, 2000);
-            } else {
-                UI.updateXvpButton(0);
-                UI.keepControlbar();
-            }
+    showStatus: function(text, status_type, time) {
+        var statusElem = document.getElementById('noVNC_status');
 
-            // Hide input related buttons in view only mode
-            if (UI.rfb && UI.rfb.get_view_only()) {
-                document.getElementById('noVNC_keyboard_button')
-                    .classList.add('noVNC_hidden');
-                document.getElementById('noVNC_toggle_extra_keys_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');
-            }
+        clearTimeout(UI.statusTimeout);
 
-            // State change disables viewport dragging.
-            // It is enabled (toggled) by direct click on the button
-            UI.setViewDrag(false);
+        if (typeof status_type === 'undefined') {
+            status_type = 'normal';
+        }
 
-            // State change also closes the password dialog
-            document.getElementById('noVNC_password_dlg')
-                .classList.remove('noVNC_open');
+        // Don't overwrite more severe visible statuses and never
+        // errors. Only shows the first error.
+        let visible_status_type = 'none';
+        if (statusElem.classList.contains("noVNC_open")) {
+            if (statusElem.classList.contains("noVNC_status_error")) {
+                visible_status_type = 'error';
+            } else if (statusElem.classList.contains("noVNC_status_warn")) {
+                visible_status_type = 'warn';
+            } else {
+                visible_status_type = 'normal';
+            }
+        }
+        if (visible_status_type === 'error' ||
+            (visible_status_type === 'warn' && status_type === 'normal')) {
+            return;
+        }
 
-            //Util.Debug("<< updateVisualState");
-        },
+        switch (status_type) {
+            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;
+        }
 
-        showStatus: function(text, status_type, time) {
-            var statusElem = document.getElementById('noVNC_status');
+        statusElem.textContent = text;
+        statusElem.classList.add("noVNC_open");
 
-            clearTimeout(UI.statusTimeout);
+        // If no time was specified, show the status for 1.5 seconds
+        if (typeof time === 'undefined') {
+            time = 1500;
+        }
 
-            if (typeof status_type === 'undefined') {
-                status_type = 'normal';
-            }
+        // Error messages do not timeout
+        if (status_type !== 'error') {
+            UI.statusTimeout = window.setTimeout(UI.hideStatus, time);
+        }
+    },
+
+    hideStatus: function() {
+        clearTimeout(UI.statusTimeout);
+        document.getElementById('noVNC_status').classList.remove("noVNC_open");
+    },
+
+    activateControlbar: function(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: function() {
+        document.getElementById('noVNC_control_bar_anchor')
+            .classList.add("noVNC_idle");
+    },
+
+    keepControlbar: function() {
+        clearTimeout(UI.closeControlbarTimeout);
+    },
+
+    openControlbar: function() {
+        document.getElementById('noVNC_control_bar')
+            .classList.add("noVNC_open");
+    },
+
+    closeControlbar: function() {
+        UI.closeAllPanels();
+        document.getElementById('noVNC_control_bar')
+            .classList.remove("noVNC_open");
+    },
+
+    toggleControlbar: function() {
+        if (document.getElementById('noVNC_control_bar')
+            .classList.contains("noVNC_open")) {
+            UI.closeControlbar();
+        } else {
+            UI.openControlbar();
+        }
+    },
+
+    toggleControlbarSide: function () {
+        // Temporarily disable animation to avoid weird movement
+        var bar = document.getElementById('noVNC_control_bar');
+        bar.style.transitionDuration = '0s';
+        bar.addEventListener('transitionend', function () { this.style.transitionDuration = ""; });
+
+        var 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");
+        }
 
-            statusElem.classList.remove("noVNC_status_normal");
-            statusElem.classList.remove("noVNC_status_warn");
-            statusElem.classList.remove("noVNC_status_error");
+        // Consider this a movement of the handle
+        UI.controlbarDrag = true;
+    },
 
-            switch (status_type) {
-                case 'warning':
-                case 'warn':
-                    statusElem.classList.add("noVNC_status_warn");
-                    break;
-                case 'error':
-                    statusElem.classList.add("noVNC_status_error");
-                    break;
-                case 'normal':
-                case 'info':
-                default:
-                    statusElem.classList.add("noVNC_status_normal");
-                    break;
-            }
+    showControlbarHint: function (show) {
+        var hint = document.getElementById('noVNC_control_bar_hint');
+        if (show) {
+            hint.classList.add("noVNC_active");
+        } else {
+            hint.classList.remove("noVNC_active");
+        }
+    },
 
-            statusElem.textContent = text;
-            statusElem.classList.add("noVNC_open");
+    dragControlbarHandle: function (e) {
+        if (!UI.controlbarGrabbed) return;
 
-            // If no time was specified, show the status for 1.5 seconds
-            if (typeof time === 'undefined') {
-                time = 1500;
-            }
+        var ptr = getPointerEvent(e);
 
-            // Error messages do not timeout
-            if (status_type !== 'error') {
-                UI.statusTimeout = window.setTimeout(UI.hideStatus, time);
+        var anchor = document.getElementById('noVNC_control_bar_anchor');
+        if (ptr.clientX < (window.innerWidth * 0.1)) {
+            if (anchor.classList.contains("noVNC_right")) {
+                UI.toggleControlbarSide();
             }
-        },
-
-        hideStatus: function() {
-            clearTimeout(UI.statusTimeout);
-            document.getElementById('noVNC_status').classList.remove("noVNC_open");
-        },
-
-        notification: function (rfb, msg, level, options) {
-            UI.showStatus(msg, level);
-        },
-
-        activateControlbar: function(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: function() {
-            document.getElementById('noVNC_control_bar_anchor')
-                .classList.add("noVNC_idle");
-        },
-
-        keepControlbar: function() {
-            clearTimeout(UI.closeControlbarTimeout);
-        },
-
-        openControlbar: function() {
-            document.getElementById('noVNC_control_bar')
-                .classList.add("noVNC_open");
-        },
-
-        closeControlbar: function() {
-            UI.closeAllPanels();
-            document.getElementById('noVNC_control_bar')
-                .classList.remove("noVNC_open");
-        },
-
-        toggleControlbar: function() {
-            if (document.getElementById('noVNC_control_bar')
-                .classList.contains("noVNC_open")) {
-                UI.closeControlbar();
-            } else {
-                UI.openControlbar();
+        } else if (ptr.clientX > (window.innerWidth * 0.9)) {
+            if (!anchor.classList.contains("noVNC_right")) {
+                UI.toggleControlbarSide();
             }
-        },
+        }
 
-        toggleControlbarSide: function () {
-            // Temporarily disable animation to avoid weird movement
-            var bar = document.getElementById('noVNC_control_bar');
-            bar.style.transitionDuration = '0s';
-            bar.addEventListener('transitionend', function () { this.style.transitionDuration = ""; });
+        if (!UI.controlbarDrag) {
+            // The goal is to trigger on a certain physical width, the
+            // devicePixelRatio brings us a bit closer but is not optimal.
+            var dragThreshold = 10 * (window.devicePixelRatio || 1);
+            var dragDistance = Math.abs(ptr.clientY - UI.controlbarMouseDownClientY);
 
-            var 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");
-            }
+            if (dragDistance < dragThreshold) return;
 
-            // Consider this a movement of the handle
             UI.controlbarDrag = true;
-        },
+        }
 
-        dragControlbarHandle: function (e) {
-            if (!UI.controlbarGrabbed) return;
+        var eventY = ptr.clientY - UI.controlbarMouseDownOffsetY;
 
-            var ptr = Util.getPointerEvent(e);
+        UI.moveControlbarHandle(eventY);
 
-            var 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();
-                }
-            }
+        e.preventDefault();
+        e.stopPropagation();
+        UI.keepControlbar();
+        UI.activateControlbar();
+    },
 
-            if (!UI.controlbarDrag) {
-                // The goal is to trigger on a certain physical width, the
-                // devicePixelRatio brings us a bit closer but is not optimal.
-                var dragThreshold = 10 * (window.devicePixelRatio || 1);
-                var dragDistance = Math.abs(ptr.clientY - UI.controlbarMouseDownClientY);
+    // Move the handle but don't allow any position outside the bounds
+    moveControlbarHandle: function (viewportRelativeY) {
+        var handle = document.getElementById("noVNC_control_bar_handle");
+        var handleHeight = handle.getBoundingClientRect().height;
+        var controlbarBounds = document.getElementById("noVNC_control_bar")
+            .getBoundingClientRect();
+        var margin = 10;
 
-                if (dragDistance < dragThreshold) return;
+        // These heights need to be non-zero for the below logic to work
+        if (handleHeight === 0 || controlbarBounds.height === 0) {
+            return;
+        }
 
-                UI.controlbarDrag = true;
-            }
+        var newY = viewportRelativeY;
 
-            var eventY = ptr.clientY - UI.controlbarMouseDownOffsetY;
+        // 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;
 
-            UI.moveControlbarHandle(eventY);
+        } 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
+        var parentRelativeY = newY - controlbarBounds.top;
+        handle.style.transform = "translateY(" + parentRelativeY + "px)";
+    },
+
+    updateControlbarHandle: function () {
+        // Since the control bar is fixed on the viewport and not the page,
+        // the move function expects coordinates relative the the viewport.
+        var handle = document.getElementById("noVNC_control_bar_handle");
+        var handleBounds = handle.getBoundingClientRect();
+        UI.moveControlbarHandle(handleBounds.top);
+    },
+
+    controlbarHandleMouseUp: function(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();
-        },
-
-        // Move the handle but don't allow any position outside the bounds
-        moveControlbarHandle: function (viewportRelativeY) {
-            var handle = document.getElementById("noVNC_control_bar_handle");
-            var handleHeight = handle.getBoundingClientRect().height;
-            var controlbarBounds = document.getElementById("noVNC_control_bar")
-                .getBoundingClientRect();
-            var margin = 10;
-
-            // These heights need to be non-zero for the below logic to work
-            if (handleHeight === 0 || controlbarBounds.height === 0) {
-                return;
-            }
-
-            var 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;
+        }
+        UI.controlbarGrabbed = false;
+        UI.showControlbarHint(false);
+    },
 
-            } 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;
-            }
+    controlbarHandleMouseDown: function(e) {
+        if ((e.type == "mousedown") && (e.button != 0)) return;
 
-            // 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
-            var parentRelativeY = newY - controlbarBounds.top;
-            handle.style.transform = "translateY(" + parentRelativeY + "px)";
-        },
-
-        updateControlbarHandle: function () {
-            // Since the control bar is fixed on the viewport and not the page,
-            // the move function expects coordinates relative the the viewport.
-            var handle = document.getElementById("noVNC_control_bar_handle");
-            var handleBounds = handle.getBoundingClientRect();
-            UI.moveControlbarHandle(handleBounds.top);
-        },
-
-        controlbarHandleMouseUp: function(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;
-        },
+        var ptr = getPointerEvent(e);
 
-        controlbarHandleMouseDown: function(e) {
-            if ((e.type == "mousedown") && (e.button != 0)) return;
+        var handle = document.getElementById("noVNC_control_bar_handle");
+        var bounds = handle.getBoundingClientRect();
 
-            var ptr = Util.getPointerEvent(e);
+        // Touch events have implicit capture
+        if (e.type === "mousedown") {
+            setCapture(handle);
+        }
 
-            var handle = document.getElementById("noVNC_control_bar_handle");
-            var bounds = handle.getBoundingClientRect();
+        UI.controlbarGrabbed = true;
+        UI.controlbarDrag = false;
 
-            Util.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();
-        },
+        UI.controlbarMouseDownClientY = ptr.clientY;
+        UI.controlbarMouseDownOffsetY = ptr.clientY - bounds.top;
+        e.preventDefault();
+        e.stopPropagation();
+        UI.keepControlbar();
+        UI.activateControlbar();
+    },
 
-        toggleExpander: function(e) {
-            if (this.classList.contains("noVNC_open")) {
-                this.classList.remove("noVNC_open");
-            } else {
-                this.classList.add("noVNC_open");
-            }
-        },
+    toggleExpander: function(e) {
+        if (this.classList.contains("noVNC_open")) {
+            this.classList.remove("noVNC_open");
+        } else {
+            this.classList.add("noVNC_open");
+        }
+    },
 
 /* ------^-------
  *    /VISUAL
@@ -787,78 +730,93 @@ var UI;
  *    SETTINGS
  * ------v------*/
 
-        // Initial page load read/initialization of settings
-        initSetting: function(name, defVal) {
-            // Check Query string followed by cookie
-            var val = WebUtil.getConfigVar(name);
-            if (val === null) {
-                val = WebUtil.readSetting(name, defVal);
-            }
-            UI.updateSetting(name, val);
-            return val;
-        },
+    // Initial page load read/initialization of settings
+    initSetting: function(name, defVal) {
+        // Check Query string followed by cookie
+        var val = WebUtil.getConfigVar(name);
+        if (val === null) {
+            val = WebUtil.readSetting(name, defVal);
+        }
+        UI.updateSetting(name, val);
+        return val;
+    },
 
-        // Update cookie and form control setting. If value is not set, then
-        // updates from control to current cookie setting.
-        updateSetting: function(name, value) {
+    // Update cookie and form control setting. If value is not set, then
+    // updates from control to current cookie setting.
+    updateSetting: function(name, value) {
 
-            // Save the cookie for this session
-            if (typeof value !== 'undefined') {
-                WebUtil.writeSetting(name, value);
-            }
+        // Save the cookie for this session
+        if (typeof value !== 'undefined') {
+            WebUtil.writeSetting(name, value);
+        }
 
-            // Update the settings control
-            value = UI.getSetting(name);
+        // Update the settings control
+        value = UI.getSetting(name);
 
-            var ctrl = document.getElementById('noVNC_setting_' + name);
-            if (ctrl.type === 'checkbox') {
-                ctrl.checked = value;
+        var ctrl = document.getElementById('noVNC_setting_' + name);
+        if (ctrl.type === 'checkbox') {
+            ctrl.checked = value;
 
-            } else if (typeof ctrl.options !== 'undefined') {
-                for (var i = 0; i < ctrl.options.length; i += 1) {
-                    if (ctrl.options[i].value === value) {
-                        ctrl.selectedIndex = i;
-                        break;
-                    }
-                }
-            } else {
-                /*Weird IE9 error leads to 'null' appearring
-                in textboxes instead of ''.*/
-                if (value === null) {
-                    value = "";
+        } else if (typeof ctrl.options !== 'undefined') {
+            for (var i = 0; i < ctrl.options.length; i += 1) {
+                if (ctrl.options[i].value === value) {
+                    ctrl.selectedIndex = i;
+                    break;
                 }
-                ctrl.value = value;
             }
-        },
-
-        // Save control setting to cookie
-        saveSetting: function(name) {
-            var val, ctrl = document.getElementById('noVNC_setting_' + name);
-            if (ctrl.type === 'checkbox') {
-                val = ctrl.checked;
-            } else if (typeof ctrl.options !== 'undefined') {
-                val = ctrl.options[ctrl.selectedIndex].value;
-            } else {
-                val = ctrl.value;
+        } else {
+            /*Weird IE9 error leads to 'null' appearring
+            in textboxes instead of ''.*/
+            if (value === null) {
+                value = "";
             }
-            WebUtil.writeSetting(name, val);
-            //Util.Debug("Setting saved '" + name + "=" + val + "'");
-            return val;
-        },
-
-        // Read form control compatible setting from cookie
-        getSetting: function(name) {
-            var ctrl = document.getElementById('noVNC_setting_' + name);
-            var 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;
-                }
+            ctrl.value = value;
+        }
+    },
+
+    // Save control setting to cookie
+    saveSetting: function(name) {
+        var val, ctrl = document.getElementById('noVNC_setting_' + name);
+        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: function(name) {
+        var ctrl = document.getElementById('noVNC_setting_' + name);
+        var 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;
-        },
+        }
+        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: function(name) {
+        var ctrl = document.getElementById('noVNC_setting_' + name);
+        ctrl.disabled = true;
+        ctrl.label.classList.add('noVNC_disabled');
+    },
+
+    enableSetting: function(name) {
+        var ctrl = document.getElementById('noVNC_setting_' + name);
+        ctrl.disabled = false;
+        ctrl.label.classList.remove('noVNC_disabled');
+    },
 
 /* ------^-------
  *   /SETTINGS
@@ -866,12 +824,12 @@ var UI;
  *    PANELS
  * ------v------*/
 
-        closeAllPanels: function() {
-            UI.closeSettingsPanel();
-            UI.closeXvpPanel();
-            UI.closeClipboardPanel();
-            UI.closeExtraKeys();
-        },
+    closeAllPanels: function() {
+        UI.closeSettingsPanel();
+        UI.closePowerPanel();
+        UI.closeClipboardPanel();
+        UI.closeExtraKeys();
+    },
 
 /* ------^-------
  *   /PANELS
@@ -879,145 +837,140 @@ var UI;
  * SETTINGS (panel)
  * ------v------*/
 
-        openSettingsPanel: function() {
-            UI.closeAllPanels();
-            UI.openControlbar();
-
-            // Refresh UI elements from saved cookies
-            UI.updateSetting('encrypt');
-            UI.updateSetting('true_color');
-            if (Util.browserSupportsCursorURIs()) {
-                UI.updateSetting('cursor');
-            } else {
-                UI.updateSetting('cursor', !Util.isTouchDevice);
-                document.getElementById('noVNC_setting_cursor').disabled = true;
-            }
-            UI.updateSetting('clip');
-            UI.updateSetting('resize');
-            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: function() {
-            document.getElementById('noVNC_settings')
-                .classList.remove("noVNC_open");
-            document.getElementById('noVNC_settings_button')
-                .classList.remove("noVNC_selected");
-        },
-
-        toggleSettingsPanel: function() {
-            if (document.getElementById('noVNC_settings')
-                .classList.contains("noVNC_open")) {
-                UI.closeSettingsPanel();
-            } else {
-                UI.openSettingsPanel();
-            }
-        },
+    openSettingsPanel: function() {
+        UI.closeAllPanels();
+        UI.openControlbar();
+
+        // Refresh UI elements from saved cookies
+        UI.updateSetting('encrypt');
+        UI.updateSetting('view_clip');
+        UI.updateSetting('resize');
+        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: function() {
+        document.getElementById('noVNC_settings')
+            .classList.remove("noVNC_open");
+        document.getElementById('noVNC_settings_button')
+            .classList.remove("noVNC_selected");
+    },
+
+    toggleSettingsPanel: function() {
+        if (document.getElementById('noVNC_settings')
+            .classList.contains("noVNC_open")) {
+            UI.closeSettingsPanel();
+        } else {
+            UI.openSettingsPanel();
+        }
+    },
 
 /* ------^-------
  *   /SETTINGS
  * ==============
- *      XVP
+ *     POWER
  * ------v------*/
 
-        openXvpPanel: function() {
-            UI.closeAllPanels();
-            UI.openControlbar();
-
-            document.getElementById('noVNC_xvp')
-                .classList.add("noVNC_open");
-            document.getElementById('noVNC_xvp_button')
-                .classList.add("noVNC_selected");
-        },
-
-        closeXvpPanel: function() {
-            document.getElementById('noVNC_xvp')
-                .classList.remove("noVNC_open");
-            document.getElementById('noVNC_xvp_button')
-                .classList.remove("noVNC_selected");
-        },
-
-        toggleXvpPanel: function() {
-            if (document.getElementById('noVNC_xvp')
-                .classList.contains("noVNC_open")) {
-                UI.closeXvpPanel();
-            } else {
-                UI.openXvpPanel();
-            }
-        },
-
-        // Disable/enable XVP button
-        updateXvpButton: function(ver) {
-            if (ver >= 1 && !UI.rfb.get_view_only()) {
-                document.getElementById('noVNC_xvp_button')
-                    .classList.remove("noVNC_hidden");
-            } else {
-                document.getElementById('noVNC_xvp_button')
-                    .classList.add("noVNC_hidden");
-                // Close XVP panel if open
-                UI.closeXvpPanel();
-            }
-        },
+    openPowerPanel: function() {
+        UI.closeAllPanels();
+        UI.openControlbar();
+
+        document.getElementById('noVNC_power')
+            .classList.add("noVNC_open");
+        document.getElementById('noVNC_power_button')
+            .classList.add("noVNC_selected");
+    },
+
+    closePowerPanel: function() {
+        document.getElementById('noVNC_power')
+            .classList.remove("noVNC_open");
+        document.getElementById('noVNC_power_button')
+            .classList.remove("noVNC_selected");
+    },
+
+    togglePowerPanel: function() {
+        if (document.getElementById('noVNC_power')
+            .classList.contains("noVNC_open")) {
+            UI.closePowerPanel();
+        } else {
+            UI.openPowerPanel();
+        }
+    },
+
+    // Disable/enable power button
+    updatePowerButton: function() {
+        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();
+        }
+    },
 
 /* ------^-------
- *     /XVP
+ *    /POWER
  * ==============
  *   CLIPBOARD
  * ------v------*/
 
-        openClipboardPanel: function() {
-            UI.closeAllPanels();
-            UI.openControlbar();
-
-            document.getElementById('noVNC_clipboard')
-                .classList.add("noVNC_open");
-            document.getElementById('noVNC_clipboard_button')
-                .classList.add("noVNC_selected");
-        },
-
-        closeClipboardPanel: function() {
-            document.getElementById('noVNC_clipboard')
-                .classList.remove("noVNC_open");
-            document.getElementById('noVNC_clipboard_button')
-                .classList.remove("noVNC_selected");
-        },
-
-        toggleClipboardPanel: function() {
-            if (document.getElementById('noVNC_clipboard')
-                .classList.contains("noVNC_open")) {
-                UI.closeClipboardPanel();
-            } else {
-                UI.openClipboardPanel();
-            }
-        },
-
-        clipboardReceive: function(rfb, text) {
-            Util.Debug(">> UI.clipboardReceive: " + text.substr(0,40) + "...");
-            document.getElementById('noVNC_clipboard_text').value = text;
-            Util.Debug("<< UI.clipboardReceive");
-        },
-
-        clipboardClear: function() {
-            document.getElementById('noVNC_clipboard_text').value = "";
-            UI.rfb.clipboardPasteFrom("");
-        },
-
-        clipboardSend: function() {
-            var text = document.getElementById('noVNC_clipboard_text').value;
-            Util.Debug(">> UI.clipboardSend: " + text.substr(0,40) + "...");
-            UI.rfb.clipboardPasteFrom(text);
-            Util.Debug("<< UI.clipboardSend");
-        },
+    openClipboardPanel: function() {
+        UI.closeAllPanels();
+        UI.openControlbar();
+
+        document.getElementById('noVNC_clipboard')
+            .classList.add("noVNC_open");
+        document.getElementById('noVNC_clipboard_button')
+            .classList.add("noVNC_selected");
+    },
+
+    closeClipboardPanel: function() {
+        document.getElementById('noVNC_clipboard')
+            .classList.remove("noVNC_open");
+        document.getElementById('noVNC_clipboard_button')
+            .classList.remove("noVNC_selected");
+    },
+
+    toggleClipboardPanel: function() {
+        if (document.getElementById('noVNC_clipboard')
+            .classList.contains("noVNC_open")) {
+            UI.closeClipboardPanel();
+        } else {
+            UI.openClipboardPanel();
+        }
+    },
+
+    clipboardReceive: function(e) {
+        Log.Debug(">> UI.clipboardReceive: " + e.detail.text.substr(0,40) + "...");
+        document.getElementById('noVNC_clipboard_text').value = e.detail.text;
+        Log.Debug("<< UI.clipboardReceive");
+    },
+
+    clipboardClear: function() {
+        document.getElementById('noVNC_clipboard_text').value = "";
+        UI.rfb.clipboardPasteFrom("");
+    },
+
+    clipboardSend: function() {
+        var text = document.getElementById('noVNC_clipboard_text').value;
+        Log.Debug(">> UI.clipboardSend: " + text.substr(0,40) + "...");
+        UI.rfb.clipboardPasteFrom(text);
+        Log.Debug("<< UI.clipboardSend");
+    },
 
 /* ------^-------
  *  /CLIPBOARD
@@ -1025,101 +978,178 @@ var UI;
  *  CONNECTION
  * ------v------*/
 
-        openConnectPanel: function() {
-            document.getElementById('noVNC_connect_dlg')
-                .classList.add("noVNC_open");
-        },
+    openConnectPanel: function() {
+        document.getElementById('noVNC_connect_dlg')
+            .classList.add("noVNC_open");
+    },
 
-        closeConnectPanel: function() {
-            document.getElementById('noVNC_connect_dlg')
-                .classList.remove("noVNC_open");
-        },
+    closeConnectPanel: function() {
+        document.getElementById('noVNC_connect_dlg')
+            .classList.remove("noVNC_open");
+    },
 
-        connect: function(event, password) {
-            var host = UI.getSetting('host');
-            var port = UI.getSetting('port');
-            var path = UI.getSetting('path');
+    connect: function(event, password) {
 
-            if (typeof password === 'undefined') {
-                password = WebUtil.getConfigVar('password');
-            }
+        // Ignore when rfb already exists
+        if (typeof UI.rfb !== 'undefined') {
+            return;
+        }
 
-            if (password === null) {
-                password = undefined;
-            }
+        var host = UI.getSetting('host');
+        var port = UI.getSetting('port');
+        var path = UI.getSetting('path');
 
-            if ((!host) || (!port)) {
-                var msg = _("Must set host and port");
-                Util.Error(msg);
-                UI.showStatus(msg, 'error');
-                return;
-            }
+        if (typeof password === 'undefined') {
+            password = WebUtil.getConfigVar('password');
+            UI.reconnect_password = password;
+        }
 
-            if (!UI.initRFB()) return;
+        if (password === null) {
+            password = undefined;
+        }
 
-            UI.closeAllPanels();
-            UI.closeConnectPanel();
+        UI.hideStatus();
 
-            UI.rfb.set_encrypt(UI.getSetting('encrypt'));
-            UI.rfb.set_true_color(UI.getSetting('true_color'));
-            UI.rfb.set_local_cursor(UI.getSetting('cursor'));
-            UI.rfb.set_shared(UI.getSetting('shared'));
-            UI.rfb.set_view_only(UI.getSetting('view_only'));
-            UI.rfb.set_repeaterID(UI.getSetting('repeaterID'));
+        if (!host) {
+            Log.Error("Can't connect when host is: " + host);
+            UI.showStatus(_("Must set host"), 'error');
+            return;
+        }
 
-            UI.rfb.connect(host, port, password, path);
-        },
+        UI.closeAllPanels();
+        UI.closeConnectPanel();
 
-        disconnect: function() {
-            UI.closeAllPanels();
-            UI.rfb.disconnect();
+        var url;
 
-            // Disable automatic reconnecting
-            UI.inhibit_reconnect = true;
+        url = UI.getSetting('encrypt') ? 'wss' : 'ws';
 
-            // Restore the callback used for initial resize
-            UI.rfb.set_onFBUComplete(UI.initialResize);
+        url += '://' + host;
+        if(port) {
+            url += ':' + port;
+        }
+        url += '/' + path;
+
+        UI.rfb = new RFB(document.getElementById('noVNC_container'), url,
+                         { shared: UI.getSetting('shared'),
+                           repeaterID: UI.getSetting('repeaterID'),
+                           credentials: { password: password } });
+        UI.rfb.addEventListener("connect", UI.connectFinished);
+        UI.rfb.addEventListener("disconnect", UI.disconnectFinished);
+        UI.rfb.addEventListener("credentialsrequired", UI.credentials);
+        UI.rfb.addEventListener("securityfailure", UI.securityFailed);
+        UI.rfb.addEventListener("capabilities", function () { 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.updateVisualState('connecting');
+        UI.updateViewOnly();
+    },
+
+    disconnect: function() {
+        UI.closeAllPanels();
+        UI.rfb.disconnect();
+
+        UI.connected = false;
+
+        // Disable automatic reconnecting
+        UI.inhibit_reconnect = true;
+
+        UI.updateVisualState('disconnecting');
+
+        UI.rfb = undefined;
+
+        // Don't display the connection settings until we're actually disconnected
+    },
+
+    reconnect: function() {
+        UI.reconnect_callback = null;
+
+        // if reconnect has been disabled in the meantime, do nothing.
+        if (UI.inhibit_reconnect) {
+            return;
+        }
 
-            // Don't display the connection settings until we're actually disconnected
-        },
+        UI.connect(null, UI.reconnect_password);
+    },
 
-        reconnect: function() {
+    cancelReconnect: function() {
+        if (UI.reconnect_callback !== null) {
+            clearTimeout(UI.reconnect_callback);
             UI.reconnect_callback = null;
+        }
 
-            // if reconnect has been disabled in the meantime, do nothing.
-            if (UI.inhibit_reconnect) {
-                return;
-            }
+        UI.updateVisualState('disconnected');
 
-            UI.connect(null, UI.reconnect_password);
-        },
+        UI.openControlbar();
+        UI.openConnectPanel();
+    },
 
-        disconnectFinished: function (rfb, reason) {
-            if (typeof reason !== 'undefined') {
-                UI.showStatus(reason, 'error');
-            } else if (UI.getSetting('reconnect', false) === true && !UI.inhibit_reconnect) {
-                document.getElementById("noVNC_transition_text").textContent = _("Reconnecting...");
-                document.documentElement.classList.add("noVNC_reconnecting");
+    connectFinished: function (e) {
+        UI.connected = true;
+        UI.inhibit_reconnect = false;
 
-                var delay = parseInt(UI.getSetting('reconnect_delay'));
-                UI.reconnect_callback = setTimeout(UI.reconnect, delay);
-                return;
+        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: function (e) {
+        let 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;
+
+        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');
             }
+        } else if (UI.getSetting('reconnect', false) === true && !UI.inhibit_reconnect) {
+            UI.updateVisualState('reconnecting');
 
-            UI.openControlbar();
-            UI.openConnectPanel();
-        },
-
-        cancelReconnect: function() {
-            if (UI.reconnect_callback !== null) {
-                clearTimeout(UI.reconnect_callback);
-                UI.reconnect_callback = null;
-            }
+            var delay = parseInt(UI.getSetting('reconnect_delay'));
+            UI.reconnect_callback = setTimeout(UI.reconnect, delay);
+            return;
+        } else {
+            UI.updateVisualState('disconnected');
+            UI.showStatus(_("Disconnected"), 'normal');
+        }
 
-            document.documentElement.classList.remove("noVNC_reconnecting");
-            UI.openControlbar();
-            UI.openConnectPanel();
-        },
+        UI.openControlbar();
+        UI.openConnectPanel();
+    },
+
+    securityFailed: function (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
@@ -1127,31 +1157,32 @@ var UI;
  *   PASSWORD
  * ------v------*/
 
-        passwordRequired: function(rfb, msg) {
-
-            document.getElementById('noVNC_password_dlg')
-                .classList.add('noVNC_open');
-
-            setTimeout(function () {
-                    document.getElementById('noVNC_password_input').focus();
-                }, 100);
-
-            if (typeof msg === 'undefined') {
-                msg = _("Password is required");
-            }
-            Util.Warn(msg);
-            UI.showStatus(msg, "warning");
-        },
-
-        setPassword: function(e) {
-            var password = document.getElementById('noVNC_password_input').value;
-            UI.rfb.sendPassword(password);
-            UI.reconnect_password = password;
-            document.getElementById('noVNC_password_dlg')
-                .classList.remove('noVNC_open');
-            // Prevent actually submitting the form
-            e.preventDefault();
-        },
+    credentials: function(e) {
+        // FIXME: handle more types
+        document.getElementById('noVNC_password_dlg')
+            .classList.add('noVNC_open');
+
+        setTimeout(function () {
+                document.getElementById('noVNC_password_input').focus();
+            }, 100);
+
+        Log.Warn("Server asked for a password");
+        UI.showStatus(_("Password is required"), "warning");
+    },
+
+    setPassword: function(e) {
+        // Prevent actually submitting the form
+        e.preventDefault();
+
+        var inputElem = document.getElementById('noVNC_password_input');
+        var password = inputElem.value;
+        // Clear the input after reading the password
+        inputElem.value = "";
+        UI.rfb.sendCredentials({ password: password });
+        UI.reconnect_password = password;
+        document.getElementById('noVNC_password_dlg')
+            .classList.remove('noVNC_open');
+    },
 
 /* ------^-------
  *  /PASSWORD
@@ -1159,47 +1190,47 @@ var UI;
  *   FULLSCREEN
  * ------v------*/
 
-        toggleFullscreen: function() {
-            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.enableDisableViewClip();
-            UI.updateFullscreenButton();
-        },
-
-        updateFullscreenButton: function() {
-            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");
+    toggleFullscreen: function() {
+        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.enableDisableViewClip();
+        UI.updateFullscreenButton();
+    },
+
+    updateFullscreenButton: function() {
+        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
@@ -1207,192 +1238,112 @@ var UI;
  *     RESIZE
  * ------v------*/
 
-        // Apply remote resizing or local scaling
-        applyResizeMode: function() {
-            if (!UI.rfb) return;
-
-            var screen = UI.screenSize();
-
-            if (screen && UI.connected && UI.rfb.get_display()) {
+    // Apply remote resizing or local scaling
+    applyResizeMode: function() {
+        if (!UI.rfb) return;
 
-                var display = UI.rfb.get_display();
-                var resizeMode = UI.getSetting('resize');
-                display.set_scale(1);
-
-                // Make sure the viewport is adjusted first
-                UI.updateViewClip();
-
-                if (resizeMode === 'remote') {
-
-                    // Request changing the resolution of the remote display to
-                    // the size of the local browser viewport.
-
-                    // In order to not send multiple requests before the browser-resize
-                    // is finished we wait 0.5 seconds before sending the request.
-                    clearTimeout(UI.resizeTimeout);
-                    UI.resizeTimeout = setTimeout(function(){
-                        // Request a remote size covering the viewport
-                        if (UI.rfb.requestDesktopSize(screen.w, screen.h)) {
-                            Util.Debug('Requested new desktop size: ' +
-                                       screen.w + 'x' + screen.h);
-                        }
-                    }, 500);
-
-                } else if (resizeMode === 'scale' || resizeMode === 'downscale') {
-                    var downscaleOnly = resizeMode === 'downscale';
-                    display.autoscale(screen.w, screen.h, downscaleOnly);
-                    UI.fixScrollbars();
-                }
-            }
-        },
-
-        // Gets the the size of the available viewport in the browser window
-        screenSize: function() {
-            var screen = document.getElementById('noVNC_screen');
-            return {w: screen.offsetWidth, h: screen.offsetHeight};
-        },
-
-        // Normally we only apply the current resize mode after a window resize
-        // event. This means that when a new connection is opened, there is no
-        // resize mode active.
-        // We have to wait until the first FBU because this is where the client
-        // will find the supported encodings of the server. Some calls later in
-        // the chain is dependant on knowing the server-capabilities.
-        initialResize: function(rfb, fbu) {
-            UI.applyResizeMode();
-            // After doing this once, we remove the callback.
-            UI.rfb.set_onFBUComplete(function() { });
-        },
+        UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale';
+        UI.rfb.resizeSession = UI.getSetting('resize') === 'remote';
+    },
 
 /* ------^-------
  *    /RESIZE
  * ==============
- *    CLIPPING
+ * VIEW CLIPPING
  * ------v------*/
 
-        // Set and configure viewport clipping
-        setViewClip: function(clip) {
-            UI.updateSetting('clip', clip);
-            UI.updateViewClip();
-        },
-
-        // Update parameters that depend on the clip setting
-        updateViewClip: function() {
-            if (!UI.rfb) return;
-
-            var display = UI.rfb.get_display();
-            var cur_clip = display.get_viewport();
-            var new_clip = UI.getSetting('clip');
-
-            var resizeSetting = UI.getSetting('resize');
-            if (resizeSetting === 'downscale' || resizeSetting === 'scale') {
-                // Disable clipping if we are scaling
-                new_clip = false;
-            } else if (Util.isTouchDevice) {
-                // Touch devices usually have shit scrollbars
-                new_clip = true;
-            }
+    // Update parameters that depend on the viewport clip setting
+    updateViewClip: function() {
+        if (!UI.rfb) return;
 
-            if (cur_clip !== new_clip) {
-                display.set_viewport(new_clip);
-            }
+        var cur_clip = UI.rfb.clipViewport;
+        var new_clip = UI.getSetting('view_clip');
 
-            var size = UI.screenSize();
+        if (isTouchDevice) {
+            // Touch devices usually have shit scrollbars
+            new_clip = true;
+        }
 
-            if (new_clip && size) {
-                // When clipping is enabled, the screen is limited to
-                // the size of the browser window.
-                display.viewportChangeSize(size.w, size.h);
-                UI.fixScrollbars();
-            }
+        if (cur_clip !== new_clip) {
+            UI.rfb.clipViewport = new_clip;
+        }
 
-            // Changing the viewport may change the state of
-            // the dragging button
-            UI.updateViewDrag();
-        },
-
-        // Handle special cases where clipping is forced on/off or locked
-        enableDisableViewClip: function() {
-            var resizeSetting = UI.getSetting('resize');
-            if (resizeSetting === 'downscale' || resizeSetting === 'scale') {
-                // Disable clipping if we are scaling
-                document.getElementById('noVNC_setting_clip').disabled = true;
-            } else {
-                document.getElementById('noVNC_setting_clip').disabled =
-                    UI.connected || Util.isTouchDevice;
-            }
-        },
+        // Changing the viewport may change the state of
+        // the dragging button
+        UI.updateViewDrag();
+    },
+
+    // Handle special cases where viewport clipping is forced on/off or locked
+    enableDisableViewClip: function() {
+        var resizeSetting = UI.getSetting('resize');
+        // Disable clipping if we are scaling, connected or on touch
+        if (resizeSetting === 'scale' ||
+            isTouchDevice) {
+            UI.disableSetting('view_clip');
+        } else {
+            UI.enableSetting('view_clip');
+        }
+    },
 
 /* ------^-------
- *   /CLIPPING
+ * /VIEW CLIPPING
  * ==============
  *    VIEWDRAG
  * ------v------*/
 
-        toggleViewDrag: function() {
-            if (!UI.rfb) return;
+    toggleViewDrag: function() {
+        if (!UI.rfb) return;
 
-            var drag = UI.rfb.get_viewportDrag();
-            UI.setViewDrag(!drag);
-         },
+        var drag = UI.rfb.dragViewport;
+        UI.setViewDrag(!drag);
+     },
 
-        // Set the view drag mode which moves the viewport on mouse drags
-        setViewDrag: function(drag) {
-            if (!UI.rfb) return;
+    // Set the view drag mode which moves the viewport on mouse drags
+    setViewDrag: function(drag) {
+        if (!UI.rfb) return;
 
-            UI.rfb.set_viewportDrag(drag);
+        UI.rfb.dragViewport = drag;
 
-            UI.updateViewDrag();
-        },
+        UI.updateViewDrag();
+    },
 
-        updateViewDrag: function() {
-            var clipping = false;
+    updateViewDrag: function() {
+        if (!UI.connected) return;
 
-            if (!UI.connected) return;
+        var viewDragButton = document.getElementById('noVNC_view_drag_button');
 
-            // Check if viewport drag is possible. It is only possible
-            // if the remote display is clipping the client display.
-            if (UI.rfb.get_display().get_viewport() &&
-                UI.rfb.get_display().clippingDisplay()) {
-                clipping = true;
-            }
+        if (!UI.rfb.clipViewport && 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;
+        }
 
-            var viewDragButton = document.getElementById('noVNC_view_drag_button');
+        if (UI.rfb.dragViewport) {
+            viewDragButton.classList.add("noVNC_selected");
+        } else {
+            viewDragButton.classList.remove("noVNC_selected");
+        }
 
-            if (!clipping &&
-                UI.rfb.get_viewportDrag()) {
-                // The size of the remote display is the same or smaller
-                // than the client display. Make sure viewport drag isn't
-                // active when it can't be used.
-                UI.rfb.set_viewportDrag(false);
-            }
+        // Different behaviour for touch vs non-touch
+        // The button is disabled instead of hidden on touch devices
+        if (isTouchDevice) {
+            viewDragButton.classList.remove("noVNC_hidden");
 
-            if (UI.rfb.get_viewportDrag()) {
-                viewDragButton.classList.add("noVNC_selected");
+            if (UI.rfb.clipViewport) {
+                viewDragButton.disabled = false;
             } else {
-                viewDragButton.classList.remove("noVNC_selected");
+                viewDragButton.disabled = true;
             }
+        } else {
+            viewDragButton.disabled = false;
 
-            // Different behaviour for touch vs non-touch
-            // The button is disabled instead of hidden on touch devices
-            if (Util.isTouchDevice) {
+            if (UI.rfb.clipViewport) {
                 viewDragButton.classList.remove("noVNC_hidden");
-
-                if (clipping) {
-                    viewDragButton.disabled = false;
-                } else {
-                    viewDragButton.disabled = true;
-                }
             } else {
-                viewDragButton.disabled = false;
-
-                if (clipping) {
-                    viewDragButton.classList.remove("noVNC_hidden");
-                } else {
-                    viewDragButton.classList.add("noVNC_hidden");
-                }
+                viewDragButton.classList.add("noVNC_hidden");
             }
-        },
+        }
+    },
 
 /* ------^-------
  *   /VIEWDRAG
@@ -1400,155 +1351,167 @@ var UI;
  *    KEYBOARD
  * ------v------*/
 
-        showVirtualKeyboard: function() {
-            if (!Util.isTouchDevice) return;
+    showVirtualKeyboard: function() {
+        if (!isTouchDevice) return;
 
-            var input = document.getElementById('noVNC_keyboardinput');
+        var input = document.getElementById('noVNC_keyboardinput');
 
-            if (document.activeElement == input) return;
+        if (document.activeElement == input) return;
 
-            input.focus();
+        input.focus();
 
-            try {
-                var l = input.value.length;
-                // Move the caret to the end
-                input.setSelectionRange(l, l);
-            } catch (err) {} // setSelectionRange is undefined in Google Chrome
-        },
+        try {
+            var l = input.value.length;
+            // Move the caret to the end
+            input.setSelectionRange(l, l);
+        } catch (err) {} // setSelectionRange is undefined in Google Chrome
+    },
 
-        hideVirtualKeyboard: function() {
-            if (!Util.isTouchDevice) return;
+    hideVirtualKeyboard: function() {
+        if (!isTouchDevice) return;
 
-            var input = document.getElementById('noVNC_keyboardinput');
+        var input = document.getElementById('noVNC_keyboardinput');
 
-            if (document.activeElement != input) return;
+        if (document.activeElement != input) return;
 
-            input.blur();
-        },
+        input.blur();
+    },
 
-        toggleVirtualKeyboard: function () {
-            if (document.getElementById('noVNC_keyboard_button')
-                .classList.contains("noVNC_selected")) {
-                UI.hideVirtualKeyboard();
-            } else {
-                UI.showVirtualKeyboard();
-            }
-        },
+    toggleVirtualKeyboard: function () {
+        if (document.getElementById('noVNC_keyboard_button')
+            .classList.contains("noVNC_selected")) {
+            UI.hideVirtualKeyboard();
+        } else {
+            UI.showVirtualKeyboard();
+        }
+    },
 
-        onfocusVirtualKeyboard: function(event) {
-            document.getElementById('noVNC_keyboard_button')
-                .classList.add("noVNC_selected");
-        },
+    onfocusVirtualKeyboard: function(event) {
+        document.getElementById('noVNC_keyboard_button')
+            .classList.add("noVNC_selected");
+        if (UI.rfb) {
+            UI.rfb.focusOnClick = false;
+        }
+    },
 
-        onblurVirtualKeyboard: function(event) {
-            document.getElementById('noVNC_keyboard_button')
-                .classList.remove("noVNC_selected");
-        },
+    onblurVirtualKeyboard: function(event) {
+        document.getElementById('noVNC_keyboard_button')
+            .classList.remove("noVNC_selected");
+        if (UI.rfb) {
+            UI.rfb.focusOnClick = true;
+        }
+    },
 
-        keepVirtualKeyboard: function(event) {
-            var input = document.getElementById('noVNC_keyboardinput');
+    keepVirtualKeyboard: function(event) {
+        var input = document.getElementById('noVNC_keyboardinput');
 
-            // Only prevent focus change if the virtual keyboard is active
-            if (document.activeElement != input) {
-                return;
-            }
+        // 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;
-                }
+        // 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();
-        },
+        event.preventDefault();
+    },
 
-        keyboardinputReset: function() {
-            var kbi = document.getElementById('noVNC_keyboardinput');
-            kbi.value = new Array(UI.defaultKeyboardinputLen).join("_");
-            UI.lastKeyboardinput = kbi.value;
-        },
+    keyboardinputReset: function() {
+        var kbi = document.getElementById('noVNC_keyboardinput');
+        kbi.value = new Array(UI.defaultKeyboardinputLen).join("_");
+        UI.lastKeyboardinput = kbi.value;
+    },
 
-        // 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: function(event) {
+    keyEvent: function (keysym, code, down) {
+        if (!UI.rfb) return;
 
-            if (!UI.rfb) return;
+        UI.rfb.sendKey(keysym, code, down);
+    },
 
-            var newValue = event.target.value;
+    // 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: function(event) {
 
-            if (!UI.lastKeyboardinput) {
-                UI.keyboardinputReset();
-            }
-            var oldValue = UI.lastKeyboardinput;
-
-            var 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;
-            }
-            var oldLen = oldValue.length;
+        if (!UI.rfb) return;
 
-            var backspaces;
-            var inputs = newLen - oldLen;
-            if (inputs < 0) {
-                backspaces = -inputs;
-            } else {
-                backspaces = 0;
-            }
+        var newValue = event.target.value;
 
-            // Compare the old string with the new to account for
-            // text-corrections or other input that modify existing text
-            var i;
-            for (i = 0; i < Math.min(oldLen, newLen); i++) {
-                if (newValue.charAt(i) != oldValue.charAt(i)) {
-                    inputs = newLen - i;
-                    backspaces = oldLen - i;
-                    break;
-                }
-            }
+        if (!UI.lastKeyboardinput) {
+            UI.keyboardinputReset();
+        }
+        var oldValue = UI.lastKeyboardinput;
 
-            // Send the key events
-            for (i = 0; i < backspaces; i++) {
-                UI.rfb.sendKey(KeyTable.XK_BackSpace);
-            }
-            for (i = newLen - inputs; i < newLen; i++) {
-                UI.rfb.sendKey(keysyms.fromUnicode(newValue.charCodeAt(i)).keysym);
-            }
+        var 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;
+        }
+        var oldLen = oldValue.length;
+
+        var backspaces;
+        var inputs = newLen - oldLen;
+        if (inputs < 0) {
+            backspaces = -inputs;
+        } else {
+            backspaces = 0;
+        }
 
-            // 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;
+        // Compare the old string with the new to account for
+        // text-corrections or other input that modify existing text
+        var i;
+        for (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 (i = 0; i < backspaces; i++) {
+            UI.rfb.sendKey(KeyTable.XK_BackSpace, "Backspace");
+        }
+        for (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
@@ -1556,65 +1519,65 @@ var UI;
  *   EXTRA KEYS
  * ------v------*/
 
-        openExtraKeys: function() {
-            UI.closeAllPanels();
-            UI.openControlbar();
-
-            document.getElementById('noVNC_modifiers')
-                .classList.add("noVNC_open");
-            document.getElementById('noVNC_toggle_extra_keys_button')
-                .classList.add("noVNC_selected");
-        },
-
-        closeExtraKeys: function() {
-            document.getElementById('noVNC_modifiers')
-                .classList.remove("noVNC_open");
-            document.getElementById('noVNC_toggle_extra_keys_button')
-                .classList.remove("noVNC_selected");
-        },
-
-        toggleExtraKeys: function() {
-            if(document.getElementById('noVNC_modifiers')
-                .classList.contains("noVNC_open")) {
-                UI.closeExtraKeys();
-            } else  {
-                UI.openExtraKeys();
-            }
-        },
-
-        sendEsc: function() {
-            UI.rfb.sendKey(KeyTable.XK_Escape);
-        },
-
-        sendTab: function() {
-            UI.rfb.sendKey(KeyTable.XK_Tab);
-        },
-
-        toggleCtrl: function() {
-            var btn = document.getElementById('noVNC_toggle_ctrl_button');
-            if (btn.classList.contains("noVNC_selected")) {
-                UI.rfb.sendKey(KeyTable.XK_Control_L, false);
-                btn.classList.remove("noVNC_selected");
-            } else {
-                UI.rfb.sendKey(KeyTable.XK_Control_L, true);
-                btn.classList.add("noVNC_selected");
-            }
-        },
-
-        toggleAlt: function() {
-            var btn = document.getElementById('noVNC_toggle_alt_button');
-            if (btn.classList.contains("noVNC_selected")) {
-                UI.rfb.sendKey(KeyTable.XK_Alt_L, false);
-                btn.classList.remove("noVNC_selected");
-            } else {
-                UI.rfb.sendKey(KeyTable.XK_Alt_L, true);
-                btn.classList.add("noVNC_selected");
-            }
-        },
+    openExtraKeys: function() {
+        UI.closeAllPanels();
+        UI.openControlbar();
+
+        document.getElementById('noVNC_modifiers')
+            .classList.add("noVNC_open");
+        document.getElementById('noVNC_toggle_extra_keys_button')
+            .classList.add("noVNC_selected");
+    },
+
+    closeExtraKeys: function() {
+        document.getElementById('noVNC_modifiers')
+            .classList.remove("noVNC_open");
+        document.getElementById('noVNC_toggle_extra_keys_button')
+            .classList.remove("noVNC_selected");
+    },
+
+    toggleExtraKeys: function() {
+        if(document.getElementById('noVNC_modifiers')
+            .classList.contains("noVNC_open")) {
+            UI.closeExtraKeys();
+        } else  {
+            UI.openExtraKeys();
+        }
+    },
+
+    sendEsc: function() {
+        UI.rfb.sendKey(KeyTable.XK_Escape, "Escape");
+    },
+
+    sendTab: function() {
+        UI.rfb.sendKey(KeyTable.XK_Tab);
+    },
+
+    toggleCtrl: function() {
+        var btn = document.getElementById('noVNC_toggle_ctrl_button');
+        if (btn.classList.contains("noVNC_selected")) {
+            UI.rfb.sendKey(KeyTable.XK_Control_L, "ControlLeft", false);
+            btn.classList.remove("noVNC_selected");
+        } else {
+            UI.rfb.sendKey(KeyTable.XK_Control_L, "ControlLeft", true);
+            btn.classList.add("noVNC_selected");
+        }
+    },
+
+    toggleAlt: function() {
+        var btn = document.getElementById('noVNC_toggle_alt_button');
+        if (btn.classList.contains("noVNC_selected")) {
+            UI.rfb.sendKey(KeyTable.XK_Alt_L, "AltLeft", false);
+            btn.classList.remove("noVNC_selected");
+        } else {
+            UI.rfb.sendKey(KeyTable.XK_Alt_L, "AltLeft", true);
+            btn.classList.add("noVNC_selected");
+        }
+    },
 
-        sendCtrlAltDel: function() {
-            UI.rfb.sendCtrlAltDel();
-        },
+    sendCtrlAltDel: function() {
+        UI.rfb.sendCtrlAltDel();
+    },
 
 /* ------^-------
  *   /EXTRA KEYS
@@ -1622,86 +1585,85 @@ var UI;
  *     MISC
  * ------v------*/
 
-        setMouseButton: function(num) {
-            var view_only = UI.rfb.get_view_only();
-            if (UI.rfb && !view_only) {
-                UI.rfb.get_mouse().set_touchButton(num);
-            }
-
-            var blist = [0, 1,2,4];
-            for (var b = 0; b < blist.length; b++) {
-                var button = document.getElementById('noVNC_mouse_button' +
-                                                     blist[b]);
-                if (blist[b] === num && !view_only) {
-                    button.classList.remove("noVNC_hidden");
-                } else {
-                    button.classList.add("noVNC_hidden");
-                }
-            }
-        },
-
-        displayBlur: function() {
-            if (UI.rfb && !UI.rfb.get_view_only()) {
-                UI.rfb.get_keyboard().set_focused(false);
-                UI.rfb.get_mouse().set_focused(false);
-            }
-        },
+    setMouseButton: function(num) {
+        var view_only = UI.rfb.viewOnly;
+        if (UI.rfb && !view_only) {
+            UI.rfb.touchButton = num;
+        }
 
-        displayFocus: function() {
-            if (UI.rfb && !UI.rfb.get_view_only()) {
-                UI.rfb.get_keyboard().set_focused(true);
-                UI.rfb.get_mouse().set_focused(true);
+        var blist = [0, 1,2,4];
+        for (var b = 0; b < blist.length; b++) {
+            var button = document.getElementById('noVNC_mouse_button' +
+                                                 blist[b]);
+            if (blist[b] === num && !view_only) {
+                button.classList.remove("noVNC_hidden");
+            } else {
+                button.classList.add("noVNC_hidden");
             }
-        },
-
-        updateLogging: function() {
-            WebUtil.init_logging(UI.getSetting('logging'));
-        },
-
-        updateSessionSize: function(rfb, width, height) {
-            UI.updateViewClip();
-            UI.fixScrollbars();
-        },
-
-        fixScrollbars: function() {
-            // This is a hack because Chrome screws up the calculation
-            // for when scrollbars are needed. So to fix it we temporarily
-            // toggle them off and on.
-            var screen = document.getElementById('noVNC_screen');
-            screen.style.overflow = 'hidden';
-            // Force Chrome to recalculate the layout by asking for
-            // an element's dimensions
-            screen.getBoundingClientRect();
-            screen.style.overflow = null;
-        },
-
-        updateDesktopName: function(rfb, name) {
-            UI.desktopName = name;
-            // Display the desktop name in the document title
-            document.title = name + " - noVNC";
-        },
-
-        bell: function(rfb) {
-            if (WebUtil.getConfigVar('bell', 'on') === 'on') {
-                document.getElementById('noVNC_bell').play();
+        }
+    },
+
+    updateViewOnly: function() {
+        if (!UI.rfb) return;
+        UI.rfb.viewOnly = UI.getSetting('view_only');
+    },
+
+    updateLogging: function() {
+        WebUtil.init_logging(UI.getSetting('logging'));
+    },
+
+    updateDesktopName: function(e) {
+        UI.desktopName = e.detail.name;
+        // Display the desktop name in the document title
+        document.title = e.detail.name + " - noVNC";
+    },
+
+    bell: function(e) {
+        if (WebUtil.getConfigVar('bell', 'on') === 'on') {
+            var promise = document.getElementById('noVNC_bell').play();
+            // The standards disagree on the return value here
+            if (promise) {
+                promise.catch(function(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: function(selectbox, text, value) {
-            var optn = document.createElement("OPTION");
-            optn.text = text;
-            optn.value = value;
-            selectbox.options.add(optn);
-        },
+    //Helper to add options to dropdown.
+    addOption: function(selectbox, text, value) {
+        var optn = document.createElement("OPTION");
+        optn.text = text;
+        optn.value = value;
+        selectbox.options.add(optn);
+    },
 
 /* ------^-------
  *    /MISC
  * ==============
  */
-    };
-
-    /* [module] UI.load(); */
-})();
+};
+
+// Set up translations
+var LINGUAS = ["de", "el", "nl", "pl", "sv", "zh"];
+l10n.setup(LINGUAS);
+if (l10n.language !== "en" && l10n.dictionary === undefined) {
+    WebUtil.fetchJSON('app/locale/' + l10n.language + '.json', function (translations) {
+        l10n.dictionary = translations;
+
+        // wait for translations to load before loading the UI
+        UI.prime();
+    }, function (err) {
+        throw err;
+    });
+} else {
+    UI.prime();
+}
 
-/* [module] export default UI; */
+export default UI;