main
Raw Download raw file
   1/*
   2 * noVNC: HTML5 VNC client
   3 * Copyright (C) 2019 The noVNC authors
   4 * Licensed under MPL 2.0 (see LICENSE.txt)
   5 *
   6 * See README.md for usage and integration instructions.
   7 */
   8
   9import * as Log from '../core/util/logging.js';
  10import _, { l10n } from './localization.js';
  11import { isTouchDevice, isMac, isIOS, isAndroid, isChromeOS, isSafari,
  12         hasScrollbarGutter, dragThreshold, browserAsyncClipboardSupport }
  13    from '../core/util/browser.js';
  14import { setCapture, getPointerEvent } from '../core/util/events.js';
  15import KeyTable from "../core/input/keysym.js";
  16import keysyms from "../core/input/keysymdef.js";
  17import Keyboard from "../core/input/keyboard.js";
  18import RFB from "../core/rfb.js";
  19import WakeLockManager from './wakelock.js';
  20import * as WebUtil from "./webutil.js";
  21
  22const PAGE_TITLE = "noVNC";
  23
  24const LINGUAS = ["cs", "de", "el", "es", "fr", "hr", "hu", "it", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "uk", "zh_CN", "zh_TW"];
  25
  26const UI = {
  27
  28    customSettings: {},
  29
  30    connected: false,
  31    desktopName: "",
  32
  33    statusTimeout: null,
  34    hideKeyboardTimeout: null,
  35    idleControlbarTimeout: null,
  36    closeControlbarTimeout: null,
  37
  38    controlbarGrabbed: false,
  39    controlbarDrag: false,
  40    controlbarMouseDownClientY: 0,
  41    controlbarMouseDownOffsetY: 0,
  42
  43    lastKeyboardinput: null,
  44    defaultKeyboardinputLen: 100,
  45
  46    inhibitReconnect: true,
  47    reconnectCallback: null,
  48    reconnectPassword: null,
  49
  50    wakeLockManager: new WakeLockManager(),
  51
  52    async start(options={}) {
  53        UI.customSettings = options.settings || {};
  54        if (UI.customSettings.defaults === undefined) {
  55            UI.customSettings.defaults = {};
  56        }
  57        if (UI.customSettings.mandatory === undefined) {
  58            UI.customSettings.mandatory = {};
  59        }
  60
  61        // Set up translations
  62        try {
  63            await l10n.setup(LINGUAS, "app/locale/");
  64        } catch (err) {
  65            Log.Error("Failed to load translations: " + err);
  66        }
  67
  68        // Initialize setting storage
  69        await WebUtil.initSettings();
  70
  71        // Wait for the page to load
  72        if (document.readyState !== "interactive" && document.readyState !== "complete") {
  73            await new Promise((resolve, reject) => {
  74                document.addEventListener('DOMContentLoaded', resolve);
  75            });
  76        }
  77
  78        UI.initSettings();
  79
  80        // Translate the DOM
  81        l10n.translateDOM();
  82
  83        // We rely on modern APIs which might not be available in an
  84        // insecure context
  85        if (!window.isSecureContext) {
  86            // FIXME: This gets hidden when connecting
  87            UI.showStatus(_("Running without HTTPS is not recommended, crashes or other issues are likely."), 'error');
  88        }
  89
  90        // Try to fetch version number
  91        try {
  92            let response = await fetch('./package.json');
  93            if (!response.ok) {
  94                throw Error("" + response.status + " " + response.statusText);
  95            }
  96
  97            let packageInfo = await response.json();
  98            Array.from(document.getElementsByClassName('noVNC_version')).forEach(el => el.innerText = packageInfo.version);
  99        } catch (err) {
 100            Log.Error("Couldn't fetch package.json: " + err);
 101            Array.from(document.getElementsByClassName('noVNC_version_wrapper'))
 102                .concat(Array.from(document.getElementsByClassName('noVNC_version_separator')))
 103                .forEach(el => el.style.display = 'none');
 104        }
 105
 106        // Adapt the interface for touch screen devices
 107        if (isTouchDevice) {
 108            // Remove the address bar
 109            setTimeout(() => window.scrollTo(0, 1), 100);
 110        }
 111
 112        // Restore control bar position
 113        if (WebUtil.readSetting('controlbar_pos') === 'right') {
 114            UI.toggleControlbarSide();
 115        }
 116
 117        UI.initFullscreen();
 118
 119        // Setup event handlers
 120        UI.addControlbarHandlers();
 121        UI.addTouchSpecificHandlers();
 122        UI.addExtraKeysHandlers();
 123        UI.addMachineHandlers();
 124        UI.addConnectionControlHandlers();
 125        UI.addClipboardHandlers();
 126        UI.addSettingsHandlers();
 127        document.getElementById("noVNC_status")
 128            .addEventListener('click', UI.hideStatus);
 129
 130        // Bootstrap fallback input handler
 131        UI.keyboardinputReset();
 132
 133        UI.openControlbar();
 134
 135        UI.updateVisualState('init');
 136
 137        document.documentElement.classList.remove("noVNC_loading");
 138
 139        let autoconnect = UI.getSetting('autoconnect');
 140        if (autoconnect === 'true' || autoconnect == '1') {
 141            autoconnect = true;
 142            UI.connect();
 143        } else {
 144            autoconnect = false;
 145            // Show the connect panel on first load unless autoconnecting
 146            UI.openConnectPanel();
 147        }
 148    },
 149
 150    initFullscreen() {
 151        // Only show the button if fullscreen is properly supported
 152        // * Safari doesn't support alphanumerical input while in fullscreen
 153        if (!isSafari() &&
 154            (document.documentElement.requestFullscreen ||
 155             document.documentElement.mozRequestFullScreen ||
 156             document.documentElement.webkitRequestFullscreen ||
 157             document.body.msRequestFullscreen)) {
 158            document.getElementById('noVNC_fullscreen_button')
 159                .classList.remove("noVNC_hidden");
 160            UI.addFullscreenHandlers();
 161        }
 162    },
 163
 164    initSettings() {
 165        // Logging selection dropdown
 166        const llevels = ['error', 'warn', 'info', 'debug'];
 167        for (let i = 0; i < llevels.length; i += 1) {
 168            UI.addOption(document.getElementById('noVNC_setting_logging'), llevels[i], llevels[i]);
 169        }
 170
 171        // Settings with immediate effects
 172        UI.initSetting('logging', 'warn');
 173        UI.updateLogging();
 174
 175        UI.setupSettingLabels();
 176
 177        /* Populate the controls if defaults are provided in the URL */
 178        UI.initSetting('host', '');
 179        UI.initSetting('port', 0);
 180        UI.initSetting('encrypt', (window.location.protocol === "https:"));
 181        UI.initSetting('password');
 182        UI.initSetting('autoconnect', false);
 183        UI.initSetting('view_clip', false);
 184        UI.initSetting('resize', 'off');
 185        UI.initSetting('quality', 6);
 186        UI.initSetting('compression', 2);
 187        UI.initSetting('shared', true);
 188        UI.initSetting('bell', 'on');
 189        UI.initSetting('view_only', false);
 190        UI.initSetting('show_dot', false);
 191        UI.initSetting('path', 'websockify');
 192        UI.initSetting('repeaterID', '');
 193        UI.initSetting('reconnect', false);
 194        UI.initSetting('reconnect_delay', 5000);
 195        UI.initSetting('keep_device_awake', false);
 196    },
 197    // Adds a link to the label elements on the corresponding input elements
 198    setupSettingLabels() {
 199        const labels = document.getElementsByTagName('LABEL');
 200        for (let i = 0; i < labels.length; i++) {
 201            const htmlFor = labels[i].htmlFor;
 202            if (htmlFor != '') {
 203                const elem = document.getElementById(htmlFor);
 204                if (elem) elem.label = labels[i];
 205            } else {
 206                // If 'for' isn't set, use the first input element child
 207                const children = labels[i].children;
 208                for (let j = 0; j < children.length; j++) {
 209                    if (children[j].form !== undefined) {
 210                        children[j].label = labels[i];
 211                        break;
 212                    }
 213                }
 214            }
 215        }
 216    },
 217
 218/* ------^-------
 219*     /INIT
 220* ==============
 221* EVENT HANDLERS
 222* ------v------*/
 223
 224    addControlbarHandlers() {
 225        document.getElementById("noVNC_control_bar")
 226            .addEventListener('mousemove', UI.activateControlbar);
 227        document.getElementById("noVNC_control_bar")
 228            .addEventListener('mouseup', UI.activateControlbar);
 229        document.getElementById("noVNC_control_bar")
 230            .addEventListener('mousedown', UI.activateControlbar);
 231        document.getElementById("noVNC_control_bar")
 232            .addEventListener('keydown', UI.activateControlbar);
 233
 234        document.getElementById("noVNC_control_bar")
 235            .addEventListener('mousedown', UI.keepControlbar);
 236        document.getElementById("noVNC_control_bar")
 237            .addEventListener('keydown', UI.keepControlbar);
 238
 239        document.getElementById("noVNC_view_drag_button")
 240            .addEventListener('click', UI.toggleViewDrag);
 241
 242        document.getElementById("noVNC_control_bar_handle")
 243            .addEventListener('mousedown', UI.controlbarHandleMouseDown);
 244        document.getElementById("noVNC_control_bar_handle")
 245            .addEventListener('mouseup', UI.controlbarHandleMouseUp);
 246        document.getElementById("noVNC_control_bar_handle")
 247            .addEventListener('mousemove', UI.dragControlbarHandle);
 248        // resize events aren't available for elements
 249        window.addEventListener('resize', UI.updateControlbarHandle);
 250
 251        const exps = document.getElementsByClassName("noVNC_expander");
 252        for (let i = 0;i < exps.length;i++) {
 253            exps[i].addEventListener('click', UI.toggleExpander);
 254        }
 255    },
 256
 257    addTouchSpecificHandlers() {
 258        document.getElementById("noVNC_keyboard_button")
 259            .addEventListener('click', UI.toggleVirtualKeyboard);
 260
 261        UI.touchKeyboard = new Keyboard(document.getElementById('noVNC_keyboardinput'));
 262        UI.touchKeyboard.onkeyevent = UI.keyEvent;
 263        UI.touchKeyboard.grab();
 264        document.getElementById("noVNC_keyboardinput")
 265            .addEventListener('input', UI.keyInput);
 266        document.getElementById("noVNC_keyboardinput")
 267            .addEventListener('focus', UI.onfocusVirtualKeyboard);
 268        document.getElementById("noVNC_keyboardinput")
 269            .addEventListener('blur', UI.onblurVirtualKeyboard);
 270        document.getElementById("noVNC_keyboardinput")
 271            .addEventListener('submit', () => false);
 272
 273        document.documentElement
 274            .addEventListener('mousedown', UI.keepVirtualKeyboard, true);
 275
 276        document.getElementById("noVNC_control_bar")
 277            .addEventListener('touchstart', UI.activateControlbar);
 278        document.getElementById("noVNC_control_bar")
 279            .addEventListener('touchmove', UI.activateControlbar);
 280        document.getElementById("noVNC_control_bar")
 281            .addEventListener('touchend', UI.activateControlbar);
 282        document.getElementById("noVNC_control_bar")
 283            .addEventListener('input', UI.activateControlbar);
 284
 285        document.getElementById("noVNC_control_bar")
 286            .addEventListener('touchstart', UI.keepControlbar);
 287        document.getElementById("noVNC_control_bar")
 288            .addEventListener('input', UI.keepControlbar);
 289
 290        document.getElementById("noVNC_control_bar_handle")
 291            .addEventListener('touchstart', UI.controlbarHandleMouseDown);
 292        document.getElementById("noVNC_control_bar_handle")
 293            .addEventListener('touchend', UI.controlbarHandleMouseUp);
 294        document.getElementById("noVNC_control_bar_handle")
 295            .addEventListener('touchmove', UI.dragControlbarHandle);
 296    },
 297
 298    addExtraKeysHandlers() {
 299        document.getElementById("noVNC_toggle_extra_keys_button")
 300            .addEventListener('click', UI.toggleExtraKeys);
 301        document.getElementById("noVNC_toggle_ctrl_button")
 302            .addEventListener('click', UI.toggleCtrl);
 303        document.getElementById("noVNC_toggle_windows_button")
 304            .addEventListener('click', UI.toggleWindows);
 305        document.getElementById("noVNC_toggle_alt_button")
 306            .addEventListener('click', UI.toggleAlt);
 307        document.getElementById("noVNC_send_tab_button")
 308            .addEventListener('click', UI.sendTab);
 309        document.getElementById("noVNC_send_esc_button")
 310            .addEventListener('click', UI.sendEsc);
 311        document.getElementById("noVNC_send_ctrl_alt_del_button")
 312            .addEventListener('click', UI.sendCtrlAltDel);
 313    },
 314
 315    addMachineHandlers() {
 316        document.getElementById("noVNC_shutdown_button")
 317            .addEventListener('click', () => UI.rfb.machineShutdown());
 318        document.getElementById("noVNC_reboot_button")
 319            .addEventListener('click', () => UI.rfb.machineReboot());
 320        document.getElementById("noVNC_reset_button")
 321            .addEventListener('click', () => UI.rfb.machineReset());
 322        document.getElementById("noVNC_power_button")
 323            .addEventListener('click', UI.togglePowerPanel);
 324    },
 325
 326    addConnectionControlHandlers() {
 327        document.getElementById("noVNC_disconnect_button")
 328            .addEventListener('click', UI.disconnect);
 329        document.getElementById("noVNC_connect_button")
 330            .addEventListener('click', UI.connect);
 331        document.getElementById("noVNC_cancel_reconnect_button")
 332            .addEventListener('click', UI.cancelReconnect);
 333
 334        document.getElementById("noVNC_approve_server_button")
 335            .addEventListener('click', UI.approveServer);
 336        document.getElementById("noVNC_reject_server_button")
 337            .addEventListener('click', UI.rejectServer);
 338        document.getElementById("noVNC_credentials_button")
 339            .addEventListener('click', UI.setCredentials);
 340    },
 341
 342    addClipboardHandlers() {
 343        document.getElementById("noVNC_clipboard_button")
 344            .addEventListener('click', UI.toggleClipboardPanel);
 345        document.getElementById("noVNC_clipboard_text")
 346            .addEventListener('change', UI.clipboardSend);
 347    },
 348
 349    // Add a call to save settings when the element changes,
 350    // unless the optional parameter changeFunc is used instead.
 351    addSettingChangeHandler(name, changeFunc) {
 352        const settingElem = document.getElementById("noVNC_setting_" + name);
 353        if (changeFunc === undefined) {
 354            changeFunc = () => UI.saveSetting(name);
 355        }
 356        settingElem.addEventListener('change', changeFunc);
 357    },
 358
 359    addSettingsHandlers() {
 360        document.getElementById("noVNC_settings_button")
 361            .addEventListener('click', UI.toggleSettingsPanel);
 362
 363        UI.addSettingChangeHandler('encrypt');
 364        UI.addSettingChangeHandler('resize');
 365        UI.addSettingChangeHandler('resize', UI.applyResizeMode);
 366        UI.addSettingChangeHandler('resize', UI.updateViewClip);
 367        UI.addSettingChangeHandler('quality');
 368        UI.addSettingChangeHandler('quality', UI.updateQuality);
 369        UI.addSettingChangeHandler('compression');
 370        UI.addSettingChangeHandler('compression', UI.updateCompression);
 371        UI.addSettingChangeHandler('view_clip');
 372        UI.addSettingChangeHandler('view_clip', UI.updateViewClip);
 373        UI.addSettingChangeHandler('shared');
 374        UI.addSettingChangeHandler('view_only');
 375        UI.addSettingChangeHandler('view_only', UI.updateViewOnly);
 376        UI.addSettingChangeHandler('show_dot');
 377        UI.addSettingChangeHandler('show_dot', UI.updateShowDotCursor);
 378        UI.addSettingChangeHandler('keep_device_awake');
 379        UI.addSettingChangeHandler('keep_device_awake', UI.updateRequestWakelock);
 380        UI.addSettingChangeHandler('host');
 381        UI.addSettingChangeHandler('port');
 382        UI.addSettingChangeHandler('path');
 383        UI.addSettingChangeHandler('repeaterID');
 384        UI.addSettingChangeHandler('logging');
 385        UI.addSettingChangeHandler('logging', UI.updateLogging);
 386        UI.addSettingChangeHandler('reconnect');
 387        UI.addSettingChangeHandler('reconnect_delay');
 388    },
 389
 390    addFullscreenHandlers() {
 391        document.getElementById("noVNC_fullscreen_button")
 392            .addEventListener('click', UI.toggleFullscreen);
 393
 394        window.addEventListener('fullscreenchange', UI.updateFullscreenButton);
 395        window.addEventListener('mozfullscreenchange', UI.updateFullscreenButton);
 396        window.addEventListener('webkitfullscreenchange', UI.updateFullscreenButton);
 397        window.addEventListener('msfullscreenchange', UI.updateFullscreenButton);
 398    },
 399
 400/* ------^-------
 401 * /EVENT HANDLERS
 402 * ==============
 403 *     VISUAL
 404 * ------v------*/
 405
 406    // Disable/enable controls depending on connection state
 407    updateVisualState(state) {
 408
 409        document.documentElement.classList.remove("noVNC_connecting");
 410        document.documentElement.classList.remove("noVNC_connected");
 411        document.documentElement.classList.remove("noVNC_disconnecting");
 412        document.documentElement.classList.remove("noVNC_reconnecting");
 413
 414        const transitionElem = document.getElementById("noVNC_transition_text");
 415        switch (state) {
 416            case 'init':
 417                break;
 418            case 'connecting':
 419                transitionElem.textContent = _("Connecting...");
 420                document.documentElement.classList.add("noVNC_connecting");
 421                break;
 422            case 'connected':
 423                document.documentElement.classList.add("noVNC_connected");
 424                break;
 425            case 'disconnecting':
 426                transitionElem.textContent = _("Disconnecting...");
 427                document.documentElement.classList.add("noVNC_disconnecting");
 428                break;
 429            case 'disconnected':
 430                break;
 431            case 'reconnecting':
 432                transitionElem.textContent = _("Reconnecting...");
 433                document.documentElement.classList.add("noVNC_reconnecting");
 434                break;
 435            default:
 436                Log.Error("Invalid visual state: " + state);
 437                UI.showStatus(_("Internal error"), 'error');
 438                return;
 439        }
 440
 441        if (UI.connected) {
 442            UI.updateViewClip();
 443
 444            UI.disableSetting('encrypt');
 445            UI.disableSetting('shared');
 446            UI.disableSetting('host');
 447            UI.disableSetting('port');
 448            UI.disableSetting('path');
 449            UI.disableSetting('repeaterID');
 450
 451            // Hide the controlbar after 2 seconds
 452            UI.closeControlbarTimeout = setTimeout(UI.closeControlbar, 2000);
 453        } else {
 454            UI.enableSetting('encrypt');
 455            UI.enableSetting('shared');
 456            UI.enableSetting('host');
 457            UI.enableSetting('port');
 458            UI.enableSetting('path');
 459            UI.enableSetting('repeaterID');
 460            UI.updatePowerButton();
 461            UI.keepControlbar();
 462        }
 463
 464        // State change closes dialogs as they may not be relevant
 465        // anymore
 466        UI.closeAllPanels();
 467        document.getElementById('noVNC_verify_server_dlg')
 468            .classList.remove('noVNC_open');
 469        document.getElementById('noVNC_credentials_dlg')
 470            .classList.remove('noVNC_open');
 471    },
 472
 473    showStatus(text, statusType, time) {
 474        const statusElem = document.getElementById('noVNC_status');
 475
 476        if (typeof statusType === 'undefined') {
 477            statusType = 'normal';
 478        }
 479
 480        // Don't overwrite more severe visible statuses and never
 481        // errors. Only shows the first error.
 482        if (statusElem.classList.contains("noVNC_open")) {
 483            if (statusElem.classList.contains("noVNC_status_error")) {
 484                return;
 485            }
 486            if (statusElem.classList.contains("noVNC_status_warn") &&
 487                statusType === 'normal') {
 488                return;
 489            }
 490        }
 491
 492        clearTimeout(UI.statusTimeout);
 493
 494        switch (statusType) {
 495            case 'error':
 496                statusElem.classList.remove("noVNC_status_warn");
 497                statusElem.classList.remove("noVNC_status_normal");
 498                statusElem.classList.add("noVNC_status_error");
 499                break;
 500            case 'warning':
 501            case 'warn':
 502                statusElem.classList.remove("noVNC_status_error");
 503                statusElem.classList.remove("noVNC_status_normal");
 504                statusElem.classList.add("noVNC_status_warn");
 505                break;
 506            case 'normal':
 507            case 'info':
 508            default:
 509                statusElem.classList.remove("noVNC_status_error");
 510                statusElem.classList.remove("noVNC_status_warn");
 511                statusElem.classList.add("noVNC_status_normal");
 512                break;
 513        }
 514
 515        statusElem.textContent = text;
 516        statusElem.classList.add("noVNC_open");
 517
 518        // If no time was specified, show the status for 1.5 seconds
 519        if (typeof time === 'undefined') {
 520            time = 1500;
 521        }
 522
 523        // Error messages do not timeout
 524        if (statusType !== 'error') {
 525            UI.statusTimeout = window.setTimeout(UI.hideStatus, time);
 526        }
 527    },
 528
 529    hideStatus() {
 530        clearTimeout(UI.statusTimeout);
 531        document.getElementById('noVNC_status').classList.remove("noVNC_open");
 532    },
 533
 534    activateControlbar(event) {
 535        clearTimeout(UI.idleControlbarTimeout);
 536        // We manipulate the anchor instead of the actual control
 537        // bar in order to avoid creating new a stacking group
 538        document.getElementById('noVNC_control_bar_anchor')
 539            .classList.remove("noVNC_idle");
 540        UI.idleControlbarTimeout = window.setTimeout(UI.idleControlbar, 2000);
 541    },
 542
 543    idleControlbar() {
 544        // Don't fade if a child of the control bar has focus
 545        if (document.getElementById('noVNC_control_bar')
 546            .contains(document.activeElement) && document.hasFocus()) {
 547            UI.activateControlbar();
 548            return;
 549        }
 550
 551        document.getElementById('noVNC_control_bar_anchor')
 552            .classList.add("noVNC_idle");
 553    },
 554
 555    keepControlbar() {
 556        clearTimeout(UI.closeControlbarTimeout);
 557    },
 558
 559    openControlbar() {
 560        document.getElementById('noVNC_control_bar')
 561            .classList.add("noVNC_open");
 562    },
 563
 564    closeControlbar() {
 565        UI.closeAllPanels();
 566        document.getElementById('noVNC_control_bar')
 567            .classList.remove("noVNC_open");
 568        UI.rfb.focus();
 569    },
 570
 571    toggleControlbar() {
 572        if (document.getElementById('noVNC_control_bar')
 573            .classList.contains("noVNC_open")) {
 574            UI.closeControlbar();
 575        } else {
 576            UI.openControlbar();
 577        }
 578    },
 579
 580    toggleControlbarSide() {
 581        // Temporarily disable animation, if bar is displayed, to avoid weird
 582        // movement. The transitionend-event will not fire when display=none.
 583        const bar = document.getElementById('noVNC_control_bar');
 584        const barDisplayStyle = window.getComputedStyle(bar).display;
 585        if (barDisplayStyle !== 'none') {
 586            bar.style.transitionDuration = '0s';
 587            bar.addEventListener('transitionend', () => bar.style.transitionDuration = '');
 588        }
 589
 590        const anchor = document.getElementById('noVNC_control_bar_anchor');
 591        if (anchor.classList.contains("noVNC_right")) {
 592            WebUtil.writeSetting('controlbar_pos', 'left');
 593            anchor.classList.remove("noVNC_right");
 594        } else {
 595            WebUtil.writeSetting('controlbar_pos', 'right');
 596            anchor.classList.add("noVNC_right");
 597        }
 598
 599        // Consider this a movement of the handle
 600        UI.controlbarDrag = true;
 601
 602        // The user has "followed" hint, let's hide it until the next drag
 603        UI.showControlbarHint(false, false);
 604    },
 605
 606    showControlbarHint(show, animate=true) {
 607        const hint = document.getElementById('noVNC_control_bar_hint');
 608
 609        if (animate) {
 610            hint.classList.remove("noVNC_notransition");
 611        } else {
 612            hint.classList.add("noVNC_notransition");
 613        }
 614
 615        if (show) {
 616            hint.classList.add("noVNC_active");
 617        } else {
 618            hint.classList.remove("noVNC_active");
 619        }
 620    },
 621
 622    dragControlbarHandle(e) {
 623        if (!UI.controlbarGrabbed) return;
 624
 625        const ptr = getPointerEvent(e);
 626
 627        const anchor = document.getElementById('noVNC_control_bar_anchor');
 628        if (ptr.clientX < (window.innerWidth * 0.1)) {
 629            if (anchor.classList.contains("noVNC_right")) {
 630                UI.toggleControlbarSide();
 631            }
 632        } else if (ptr.clientX > (window.innerWidth * 0.9)) {
 633            if (!anchor.classList.contains("noVNC_right")) {
 634                UI.toggleControlbarSide();
 635            }
 636        }
 637
 638        if (!UI.controlbarDrag) {
 639            const dragDistance = Math.abs(ptr.clientY - UI.controlbarMouseDownClientY);
 640
 641            if (dragDistance < dragThreshold) return;
 642
 643            UI.controlbarDrag = true;
 644        }
 645
 646        const eventY = ptr.clientY - UI.controlbarMouseDownOffsetY;
 647
 648        UI.moveControlbarHandle(eventY);
 649
 650        e.preventDefault();
 651        e.stopPropagation();
 652        UI.keepControlbar();
 653        UI.activateControlbar();
 654    },
 655
 656    // Move the handle but don't allow any position outside the bounds
 657    moveControlbarHandle(viewportRelativeY) {
 658        const handle = document.getElementById("noVNC_control_bar_handle");
 659        const handleHeight = handle.getBoundingClientRect().height;
 660        const controlbarBounds = document.getElementById("noVNC_control_bar")
 661            .getBoundingClientRect();
 662        const margin = 10;
 663
 664        // These heights need to be non-zero for the below logic to work
 665        if (handleHeight === 0 || controlbarBounds.height === 0) {
 666            return;
 667        }
 668
 669        let newY = viewportRelativeY;
 670
 671        // Check if the coordinates are outside the control bar
 672        if (newY < controlbarBounds.top + margin) {
 673            // Force coordinates to be below the top of the control bar
 674            newY = controlbarBounds.top + margin;
 675
 676        } else if (newY > controlbarBounds.top +
 677                   controlbarBounds.height - handleHeight - margin) {
 678            // Force coordinates to be above the bottom of the control bar
 679            newY = controlbarBounds.top +
 680                controlbarBounds.height - handleHeight - margin;
 681        }
 682
 683        // Corner case: control bar too small for stable position
 684        if (controlbarBounds.height < (handleHeight + margin * 2)) {
 685            newY = controlbarBounds.top +
 686                (controlbarBounds.height - handleHeight) / 2;
 687        }
 688
 689        // The transform needs coordinates that are relative to the parent
 690        const parentRelativeY = newY - controlbarBounds.top;
 691        handle.style.transform = "translateY(" + parentRelativeY + "px)";
 692    },
 693
 694    updateControlbarHandle() {
 695        // Since the control bar is fixed on the viewport and not the page,
 696        // the move function expects coordinates relative the the viewport.
 697        const handle = document.getElementById("noVNC_control_bar_handle");
 698        const handleBounds = handle.getBoundingClientRect();
 699        UI.moveControlbarHandle(handleBounds.top);
 700    },
 701
 702    controlbarHandleMouseUp(e) {
 703        if ((e.type == "mouseup") && (e.button != 0)) return;
 704
 705        // mouseup and mousedown on the same place toggles the controlbar
 706        if (UI.controlbarGrabbed && !UI.controlbarDrag) {
 707            UI.toggleControlbar();
 708            e.preventDefault();
 709            e.stopPropagation();
 710            UI.keepControlbar();
 711            UI.activateControlbar();
 712        }
 713        UI.controlbarGrabbed = false;
 714        UI.showControlbarHint(false);
 715    },
 716
 717    controlbarHandleMouseDown(e) {
 718        if ((e.type == "mousedown") && (e.button != 0)) return;
 719
 720        const ptr = getPointerEvent(e);
 721
 722        const handle = document.getElementById("noVNC_control_bar_handle");
 723        const bounds = handle.getBoundingClientRect();
 724
 725        // Touch events have implicit capture
 726        if (e.type === "mousedown") {
 727            setCapture(handle);
 728        }
 729
 730        UI.controlbarGrabbed = true;
 731        UI.controlbarDrag = false;
 732
 733        UI.showControlbarHint(true);
 734
 735        UI.controlbarMouseDownClientY = ptr.clientY;
 736        UI.controlbarMouseDownOffsetY = ptr.clientY - bounds.top;
 737        e.preventDefault();
 738        e.stopPropagation();
 739        UI.keepControlbar();
 740        UI.activateControlbar();
 741    },
 742
 743    toggleExpander(e) {
 744        if (this.classList.contains("noVNC_open")) {
 745            this.classList.remove("noVNC_open");
 746        } else {
 747            this.classList.add("noVNC_open");
 748        }
 749    },
 750
 751/* ------^-------
 752 *    /VISUAL
 753 * ==============
 754 *    SETTINGS
 755 * ------v------*/
 756
 757    // Initial page load read/initialization of settings
 758    initSetting(name, defVal) {
 759        // Has the user overridden the default value?
 760        if (name in UI.customSettings.defaults) {
 761            defVal = UI.customSettings.defaults[name];
 762        }
 763        // Check Query string followed by cookie
 764        let val = WebUtil.getConfigVar(name);
 765        if (val === null) {
 766            val = WebUtil.readSetting(name, defVal);
 767        }
 768        WebUtil.setSetting(name, val);
 769        UI.updateSetting(name);
 770        // Has the user forced a value?
 771        if (name in UI.customSettings.mandatory) {
 772            val = UI.customSettings.mandatory[name];
 773            UI.forceSetting(name, val);
 774        }
 775        return val;
 776    },
 777
 778    // Set the new value, update and disable form control setting
 779    forceSetting(name, val) {
 780        WebUtil.setSetting(name, val);
 781        UI.updateSetting(name);
 782        UI.disableSetting(name);
 783    },
 784
 785    // Update cookie and form control setting. If value is not set, then
 786    // updates from control to current cookie setting.
 787    updateSetting(name) {
 788
 789        // Update the settings control
 790        let value = UI.getSetting(name);
 791
 792        const ctrl = document.getElementById('noVNC_setting_' + name);
 793        if (ctrl === null) {
 794            return;
 795        }
 796
 797        if (ctrl.type === 'checkbox') {
 798            ctrl.checked = value;
 799        } else if (typeof ctrl.options !== 'undefined') {
 800            for (let i = 0; i < ctrl.options.length; i += 1) {
 801                if (ctrl.options[i].value === value) {
 802                    ctrl.selectedIndex = i;
 803                    break;
 804                }
 805            }
 806        } else {
 807            ctrl.value = value;
 808        }
 809    },
 810
 811    // Save control setting to cookie
 812    saveSetting(name) {
 813        const ctrl = document.getElementById('noVNC_setting_' + name);
 814        let val;
 815        if (ctrl.type === 'checkbox') {
 816            val = ctrl.checked;
 817        } else if (typeof ctrl.options !== 'undefined') {
 818            val = ctrl.options[ctrl.selectedIndex].value;
 819        } else {
 820            val = ctrl.value;
 821        }
 822        WebUtil.writeSetting(name, val);
 823        //Log.Debug("Setting saved '" + name + "=" + val + "'");
 824        return val;
 825    },
 826
 827    // Read form control compatible setting from cookie
 828    getSetting(name) {
 829        const ctrl = document.getElementById('noVNC_setting_' + name);
 830        let val = WebUtil.readSetting(name);
 831        if (typeof val !== 'undefined' && val !== null &&
 832            ctrl !== null && ctrl.type === 'checkbox') {
 833            if (val.toString().toLowerCase() in {'0': 1, 'no': 1, 'false': 1}) {
 834                val = false;
 835            } else {
 836                val = true;
 837            }
 838        }
 839        return val;
 840    },
 841
 842    // These helpers compensate for the lack of parent-selectors and
 843    // previous-sibling-selectors in CSS which are needed when we want to
 844    // disable the labels that belong to disabled input elements.
 845    disableSetting(name) {
 846        const ctrl = document.getElementById('noVNC_setting_' + name);
 847        if (ctrl !== null) {
 848            ctrl.disabled = true;
 849            if (ctrl.label !== undefined) {
 850                ctrl.label.classList.add('noVNC_disabled');
 851            }
 852        }
 853    },
 854
 855    enableSetting(name) {
 856        const ctrl = document.getElementById('noVNC_setting_' + name);
 857        if (ctrl !== null) {
 858            ctrl.disabled = false;
 859            if (ctrl.label !== undefined) {
 860                ctrl.label.classList.remove('noVNC_disabled');
 861            }
 862        }
 863    },
 864
 865/* ------^-------
 866 *   /SETTINGS
 867 * ==============
 868 *    PANELS
 869 * ------v------*/
 870
 871    closeAllPanels() {
 872        UI.closeSettingsPanel();
 873        UI.closePowerPanel();
 874        UI.closeClipboardPanel();
 875        UI.closeExtraKeys();
 876    },
 877
 878/* ------^-------
 879 *   /PANELS
 880 * ==============
 881 * SETTINGS (panel)
 882 * ------v------*/
 883
 884    openSettingsPanel() {
 885        UI.closeAllPanels();
 886        UI.openControlbar();
 887
 888        // Refresh UI elements from saved cookies
 889        UI.updateSetting('encrypt');
 890        UI.updateSetting('view_clip');
 891        UI.updateSetting('resize');
 892        UI.updateSetting('quality');
 893        UI.updateSetting('compression');
 894        UI.updateSetting('shared');
 895        UI.updateSetting('view_only');
 896        UI.updateSetting('path');
 897        UI.updateSetting('repeaterID');
 898        UI.updateSetting('logging');
 899        UI.updateSetting('reconnect');
 900        UI.updateSetting('reconnect_delay');
 901
 902        document.getElementById('noVNC_settings')
 903            .classList.add("noVNC_open");
 904        document.getElementById('noVNC_settings_button')
 905            .classList.add("noVNC_selected");
 906    },
 907
 908    closeSettingsPanel() {
 909        document.getElementById('noVNC_settings')
 910            .classList.remove("noVNC_open");
 911        document.getElementById('noVNC_settings_button')
 912            .classList.remove("noVNC_selected");
 913    },
 914
 915    toggleSettingsPanel() {
 916        if (document.getElementById('noVNC_settings')
 917            .classList.contains("noVNC_open")) {
 918            UI.closeSettingsPanel();
 919        } else {
 920            UI.openSettingsPanel();
 921        }
 922    },
 923
 924/* ------^-------
 925 *   /SETTINGS
 926 * ==============
 927 *     POWER
 928 * ------v------*/
 929
 930    openPowerPanel() {
 931        UI.closeAllPanels();
 932        UI.openControlbar();
 933
 934        document.getElementById('noVNC_power')
 935            .classList.add("noVNC_open");
 936        document.getElementById('noVNC_power_button')
 937            .classList.add("noVNC_selected");
 938    },
 939
 940    closePowerPanel() {
 941        document.getElementById('noVNC_power')
 942            .classList.remove("noVNC_open");
 943        document.getElementById('noVNC_power_button')
 944            .classList.remove("noVNC_selected");
 945    },
 946
 947    togglePowerPanel() {
 948        if (document.getElementById('noVNC_power')
 949            .classList.contains("noVNC_open")) {
 950            UI.closePowerPanel();
 951        } else {
 952            UI.openPowerPanel();
 953        }
 954    },
 955
 956    // Disable/enable power button
 957    updatePowerButton() {
 958        if (UI.connected &&
 959            UI.rfb.capabilities.power &&
 960            !UI.rfb.viewOnly) {
 961            document.getElementById('noVNC_power_button')
 962                .classList.remove("noVNC_hidden");
 963        } else {
 964            document.getElementById('noVNC_power_button')
 965                .classList.add("noVNC_hidden");
 966            // Close power panel if open
 967            UI.closePowerPanel();
 968        }
 969    },
 970
 971/* ------^-------
 972 *    /POWER
 973 * ==============
 974 *   CLIPBOARD
 975 * ------v------*/
 976
 977    openClipboardPanel() {
 978        UI.closeAllPanels();
 979        UI.openControlbar();
 980
 981        document.getElementById('noVNC_clipboard')
 982            .classList.add("noVNC_open");
 983        document.getElementById('noVNC_clipboard_button')
 984            .classList.add("noVNC_selected");
 985    },
 986
 987    closeClipboardPanel() {
 988        document.getElementById('noVNC_clipboard')
 989            .classList.remove("noVNC_open");
 990        document.getElementById('noVNC_clipboard_button')
 991            .classList.remove("noVNC_selected");
 992    },
 993
 994    toggleClipboardPanel() {
 995        if (document.getElementById('noVNC_clipboard')
 996            .classList.contains("noVNC_open")) {
 997            UI.closeClipboardPanel();
 998        } else {
 999            UI.openClipboardPanel();
1000        }
1001    },
1002
1003    clipboardReceive(e) {
1004        Log.Debug(">> UI.clipboardReceive: " + e.detail.text.substr(0, 40) + "...");
1005        document.getElementById('noVNC_clipboard_text').value = e.detail.text;
1006        Log.Debug("<< UI.clipboardReceive");
1007    },
1008
1009    clipboardSend() {
1010        const text = document.getElementById('noVNC_clipboard_text').value;
1011        Log.Debug(">> UI.clipboardSend: " + text.substr(0, 40) + "...");
1012        UI.rfb.clipboardPasteFrom(text);
1013        Log.Debug("<< UI.clipboardSend");
1014    },
1015
1016/* ------^-------
1017 *  /CLIPBOARD
1018 * ==============
1019 *  CONNECTION
1020 * ------v------*/
1021
1022    openConnectPanel() {
1023        document.getElementById('noVNC_connect_dlg')
1024            .classList.add("noVNC_open");
1025    },
1026
1027    closeConnectPanel() {
1028        document.getElementById('noVNC_connect_dlg')
1029            .classList.remove("noVNC_open");
1030    },
1031
1032    connect(event, password) {
1033
1034        // Ignore when rfb already exists
1035        if (typeof UI.rfb !== 'undefined') {
1036            return;
1037        }
1038
1039        const host = UI.getSetting('host');
1040        const port = UI.getSetting('port');
1041        const path = UI.getSetting('path');
1042
1043        if (typeof password === 'undefined') {
1044            password = UI.getSetting('password');
1045            UI.reconnectPassword = password;
1046        }
1047
1048        if (password === null) {
1049            password = undefined;
1050        }
1051
1052        UI.hideStatus();
1053
1054        UI.closeConnectPanel();
1055
1056        UI.updateVisualState('connecting');
1057
1058        let url;
1059
1060        if (host) {
1061            url = new URL("https://" + host);
1062
1063            url.protocol = UI.getSetting('encrypt') ? 'wss:' : 'ws:';
1064            if (port) {
1065                url.port = port;
1066            }
1067
1068            // "./" is needed to force URL() to interpret the path-variable as
1069            // a path and not as an URL. This is relevant if for example path
1070            // starts with more than one "/", in which case it would be
1071            // interpreted as a host name instead.
1072            url = new URL("./" + path, url);
1073        } else {
1074            // Current (May 2024) browsers support relative WebSocket
1075            // URLs natively, but we need to support older browsers for
1076            // some time.
1077            url = new URL(path, location.href);
1078            url.protocol = (window.location.protocol === "https:") ? 'wss:' : 'ws:';
1079        }
1080
1081        if (UI.getSetting('keep_device_awake')) {
1082            UI.wakeLockManager.acquire();
1083        }
1084
1085        try {
1086            UI.rfb = new RFB(document.getElementById('noVNC_container'),
1087                             url.href,
1088                             { shared: UI.getSetting('shared'),
1089                               repeaterID: UI.getSetting('repeaterID'),
1090                               credentials: { password: password } });
1091        } catch (exc) {
1092            Log.Error("Failed to connect to server: " + exc);
1093            UI.updateVisualState('disconnected');
1094            UI.showStatus(_("Failed to connect to server: ") + exc, 'error');
1095            return;
1096        }
1097
1098        UI.rfb.addEventListener("connect", UI.connectFinished);
1099        UI.rfb.addEventListener("disconnect", UI.disconnectFinished);
1100        UI.rfb.addEventListener("serververification", UI.serverVerify);
1101        UI.rfb.addEventListener("credentialsrequired", UI.credentials);
1102        UI.rfb.addEventListener("securityfailure", UI.securityFailed);
1103        UI.rfb.addEventListener("clippingviewport", UI.updateViewDrag);
1104        UI.rfb.addEventListener("capabilities", UI.updatePowerButton);
1105        UI.rfb.addEventListener("clipboard", UI.clipboardReceive);
1106        UI.rfb.addEventListener("bell", UI.bell);
1107        UI.rfb.addEventListener("desktopname", UI.updateDesktopName);
1108        UI.rfb.clipViewport = UI.getSetting('view_clip');
1109        UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale';
1110        UI.rfb.resizeSession = UI.getSetting('resize') === 'remote';
1111        UI.rfb.qualityLevel = parseInt(UI.getSetting('quality'));
1112        UI.rfb.compressionLevel = parseInt(UI.getSetting('compression'));
1113        UI.rfb.showDotCursor = UI.getSetting('show_dot');
1114
1115        UI.updateViewOnly(); // requires UI.rfb
1116        UI.updateClipboard();
1117    },
1118
1119    disconnect() {
1120        UI.rfb.disconnect();
1121
1122        UI.connected = false;
1123
1124        // Disable automatic reconnecting
1125        UI.inhibitReconnect = true;
1126
1127        UI.updateVisualState('disconnecting');
1128
1129        // Don't display the connection settings until we're actually disconnected
1130    },
1131
1132    reconnect() {
1133        UI.reconnectCallback = null;
1134
1135        // if reconnect has been disabled in the meantime, do nothing.
1136        if (UI.inhibitReconnect) {
1137            return;
1138        }
1139
1140        UI.connect(null, UI.reconnectPassword);
1141    },
1142
1143    cancelReconnect() {
1144        if (UI.reconnectCallback !== null) {
1145            clearTimeout(UI.reconnectCallback);
1146            UI.reconnectCallback = null;
1147        }
1148
1149        UI.updateVisualState('disconnected');
1150
1151        UI.openControlbar();
1152        UI.openConnectPanel();
1153    },
1154
1155    connectFinished(e) {
1156        UI.connected = true;
1157        UI.inhibitReconnect = false;
1158
1159        let msg;
1160        if (UI.getSetting('encrypt')) {
1161            msg = _("Connected (encrypted) to ") + UI.desktopName;
1162        } else {
1163            msg = _("Connected (unencrypted) to ") + UI.desktopName;
1164        }
1165        UI.showStatus(msg);
1166        UI.updateVisualState('connected');
1167
1168        UI.updateBeforeUnload();
1169
1170        // Do this last because it can only be used on rendered elements
1171        UI.rfb.focus();
1172    },
1173
1174    disconnectFinished(e) {
1175        const wasConnected = UI.connected;
1176
1177        // This variable is ideally set when disconnection starts, but
1178        // when the disconnection isn't clean or if it is initiated by
1179        // the server, we need to do it here as well since
1180        // UI.disconnect() won't be used in those cases.
1181        UI.connected = false;
1182
1183        UI.rfb = undefined;
1184        UI.wakeLockManager.release();
1185
1186        if (!e.detail.clean) {
1187            UI.updateVisualState('disconnected');
1188            if (wasConnected) {
1189                UI.showStatus(_("Something went wrong, connection is closed"),
1190                              'error');
1191            } else {
1192                UI.showStatus(_("Failed to connect to server"), 'error');
1193            }
1194        }
1195        // If reconnecting is allowed process it now
1196        if (UI.getSetting('reconnect', false) === true && !UI.inhibitReconnect) {
1197            UI.updateVisualState('reconnecting');
1198
1199            const delay = parseInt(UI.getSetting('reconnect_delay'));
1200            UI.reconnectCallback = setTimeout(UI.reconnect, delay);
1201            return;
1202        } else {
1203            UI.updateVisualState('disconnected');
1204            UI.showStatus(_("Disconnected"), 'normal');
1205        }
1206
1207        UI.updateBeforeUnload();
1208
1209        document.title = PAGE_TITLE;
1210
1211        UI.openControlbar();
1212        UI.openConnectPanel();
1213    },
1214
1215    securityFailed(e) {
1216        let msg = "";
1217        // On security failures we might get a string with a reason
1218        // directly from the server. Note that we can't control if
1219        // this string is translated or not.
1220        if ('reason' in e.detail) {
1221            msg = _("New connection has been rejected with reason: ") +
1222                e.detail.reason;
1223        } else {
1224            msg = _("New connection has been rejected");
1225        }
1226        UI.showStatus(msg, 'error');
1227    },
1228
1229    handleBeforeUnload(e) {
1230        // Trigger a "Leave site?" warning prompt before closing the
1231        // page. Modern browsers (Oct 2025) accept either (or both)
1232        // preventDefault() or a nonempty returnValue, though the latter is
1233        // considered legacy. The custom string is ignored by modern browsers,
1234        // which display a native message, but older browsers will show it.
1235        e.preventDefault();
1236        e.returnValue = _("Are you sure you want to disconnect the session?");
1237    },
1238
1239    updateBeforeUnload() {
1240        // Remove first to avoid adding duplicates
1241        window.removeEventListener("beforeunload", UI.handleBeforeUnload);
1242        if (!UI.rfb?.viewOnly && UI.connected) {
1243            window.addEventListener("beforeunload", UI.handleBeforeUnload);
1244        }
1245    },
1246
1247/* ------^-------
1248 *  /CONNECTION
1249 * ==============
1250 * SERVER VERIFY
1251 * ------v------*/
1252
1253    async serverVerify(e) {
1254        const type = e.detail.type;
1255        if (type === 'RSA') {
1256            const publickey = e.detail.publickey;
1257            let fingerprint = await window.crypto.subtle.digest("SHA-1", publickey);
1258            // The same fingerprint format as RealVNC
1259            fingerprint = Array.from(new Uint8Array(fingerprint).slice(0, 8)).map(
1260                x => x.toString(16).padStart(2, '0')).join('-');
1261            document.getElementById('noVNC_verify_server_dlg').classList.add('noVNC_open');
1262            document.getElementById('noVNC_fingerprint').innerHTML = fingerprint;
1263        }
1264    },
1265
1266    approveServer(e) {
1267        e.preventDefault();
1268        document.getElementById('noVNC_verify_server_dlg').classList.remove('noVNC_open');
1269        UI.rfb.approveServer();
1270    },
1271
1272    rejectServer(e) {
1273        e.preventDefault();
1274        document.getElementById('noVNC_verify_server_dlg').classList.remove('noVNC_open');
1275        UI.disconnect();
1276    },
1277
1278/* ------^-------
1279 * /SERVER VERIFY
1280 * ==============
1281 *   PASSWORD
1282 * ------v------*/
1283
1284    credentials(e) {
1285        // FIXME: handle more types
1286
1287        document.getElementById("noVNC_username_block").classList.remove("noVNC_hidden");
1288        document.getElementById("noVNC_password_block").classList.remove("noVNC_hidden");
1289
1290        let inputFocus = "none";
1291        if (e.detail.types.indexOf("username") === -1) {
1292            document.getElementById("noVNC_username_block").classList.add("noVNC_hidden");
1293        } else {
1294            inputFocus = inputFocus === "none" ? "noVNC_username_input" : inputFocus;
1295        }
1296        if (e.detail.types.indexOf("password") === -1) {
1297            document.getElementById("noVNC_password_block").classList.add("noVNC_hidden");
1298        } else {
1299            inputFocus = inputFocus === "none" ? "noVNC_password_input" : inputFocus;
1300        }
1301        document.getElementById('noVNC_credentials_dlg')
1302            .classList.add('noVNC_open');
1303
1304        setTimeout(() => document
1305            .getElementById(inputFocus).focus(), 100);
1306
1307        Log.Warn("Server asked for credentials");
1308        UI.showStatus(_("Credentials are required"), "warning");
1309    },
1310
1311    setCredentials(e) {
1312        // Prevent actually submitting the form
1313        e.preventDefault();
1314
1315        let inputElemUsername = document.getElementById('noVNC_username_input');
1316        const username = inputElemUsername.value;
1317
1318        let inputElemPassword = document.getElementById('noVNC_password_input');
1319        const password = inputElemPassword.value;
1320        // Clear the input after reading the password
1321        inputElemPassword.value = "";
1322
1323        UI.rfb.sendCredentials({ username: username, password: password });
1324        UI.reconnectPassword = password;
1325        document.getElementById('noVNC_credentials_dlg')
1326            .classList.remove('noVNC_open');
1327    },
1328
1329/* ------^-------
1330 *  /PASSWORD
1331 * ==============
1332 *   FULLSCREEN
1333 * ------v------*/
1334
1335    toggleFullscreen() {
1336        if (document.fullscreenElement || // alternative standard method
1337            document.mozFullScreenElement || // currently working methods
1338            document.webkitFullscreenElement ||
1339            document.msFullscreenElement) {
1340            if (document.exitFullscreen) {
1341                document.exitFullscreen();
1342            } else if (document.mozCancelFullScreen) {
1343                document.mozCancelFullScreen();
1344            } else if (document.webkitExitFullscreen) {
1345                document.webkitExitFullscreen();
1346            } else if (document.msExitFullscreen) {
1347                document.msExitFullscreen();
1348            }
1349        } else {
1350            if (document.documentElement.requestFullscreen) {
1351                document.documentElement.requestFullscreen();
1352            } else if (document.documentElement.mozRequestFullScreen) {
1353                document.documentElement.mozRequestFullScreen();
1354            } else if (document.documentElement.webkitRequestFullscreen) {
1355                document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
1356            } else if (document.body.msRequestFullscreen) {
1357                document.body.msRequestFullscreen();
1358            }
1359        }
1360        UI.updateFullscreenButton();
1361    },
1362
1363    updateFullscreenButton() {
1364        if (document.fullscreenElement || // alternative standard method
1365            document.mozFullScreenElement || // currently working methods
1366            document.webkitFullscreenElement ||
1367            document.msFullscreenElement ) {
1368            document.getElementById('noVNC_fullscreen_button')
1369                .classList.add("noVNC_selected");
1370        } else {
1371            document.getElementById('noVNC_fullscreen_button')
1372                .classList.remove("noVNC_selected");
1373        }
1374    },
1375
1376/* ------^-------
1377 *  /FULLSCREEN
1378 * ==============
1379 *     RESIZE
1380 * ------v------*/
1381
1382    // Apply remote resizing or local scaling
1383    applyResizeMode() {
1384        if (!UI.rfb) return;
1385
1386        UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale';
1387        UI.rfb.resizeSession = UI.getSetting('resize') === 'remote';
1388    },
1389
1390/* ------^-------
1391 *    /RESIZE
1392 * ==============
1393 * VIEW CLIPPING
1394 * ------v------*/
1395
1396    // Update viewport clipping property for the connection. The normal
1397    // case is to get the value from the setting. There are special cases
1398    // for when the viewport is scaled or when a touch device is used.
1399    updateViewClip() {
1400        if (!UI.rfb) return;
1401
1402        const scaling = UI.getSetting('resize') === 'scale';
1403
1404        // Some platforms have overlay scrollbars that are difficult
1405        // to use in our case, which means we have to force panning
1406        // FIXME: Working scrollbars can still be annoying to use with
1407        //        touch, so we should ideally be able to have both
1408        //        panning and scrollbars at the same time
1409
1410        let brokenScrollbars = false;
1411
1412        if (!hasScrollbarGutter) {
1413            if (isIOS() || isAndroid() || isMac() || isChromeOS()) {
1414                brokenScrollbars = true;
1415            }
1416        }
1417
1418        if (scaling) {
1419            // Can't be clipping if viewport is scaled to fit
1420            UI.forceSetting('view_clip', false);
1421            UI.rfb.clipViewport  = false;
1422        } else if (brokenScrollbars) {
1423            UI.forceSetting('view_clip', true);
1424            UI.rfb.clipViewport = true;
1425        } else {
1426            UI.enableSetting('view_clip');
1427            UI.rfb.clipViewport = UI.getSetting('view_clip');
1428        }
1429
1430        // Changing the viewport may change the state of
1431        // the dragging button
1432        UI.updateViewDrag();
1433    },
1434
1435/* ------^-------
1436 * /VIEW CLIPPING
1437 * ==============
1438 *    VIEWDRAG
1439 * ------v------*/
1440
1441    toggleViewDrag() {
1442        if (!UI.rfb) return;
1443
1444        UI.rfb.dragViewport = !UI.rfb.dragViewport;
1445        UI.updateViewDrag();
1446    },
1447
1448    updateViewDrag() {
1449        if (!UI.connected) return;
1450
1451        const viewDragButton = document.getElementById('noVNC_view_drag_button');
1452
1453        if ((!UI.rfb.clipViewport || !UI.rfb.clippingViewport) &&
1454            UI.rfb.dragViewport) {
1455            // We are no longer clipping the viewport. Make sure
1456            // viewport drag isn't active when it can't be used.
1457            UI.rfb.dragViewport = false;
1458        }
1459
1460        if (UI.rfb.dragViewport) {
1461            viewDragButton.classList.add("noVNC_selected");
1462        } else {
1463            viewDragButton.classList.remove("noVNC_selected");
1464        }
1465
1466        if (UI.rfb.clipViewport) {
1467            viewDragButton.classList.remove("noVNC_hidden");
1468        } else {
1469            viewDragButton.classList.add("noVNC_hidden");
1470        }
1471
1472        viewDragButton.disabled = !UI.rfb.clippingViewport;
1473    },
1474
1475/* ------^-------
1476 *   /VIEWDRAG
1477 * ==============
1478 *    QUALITY
1479 * ------v------*/
1480
1481    updateQuality() {
1482        if (!UI.rfb) return;
1483
1484        UI.rfb.qualityLevel = parseInt(UI.getSetting('quality'));
1485    },
1486
1487/* ------^-------
1488 *   /QUALITY
1489 * ==============
1490 *  COMPRESSION
1491 * ------v------*/
1492
1493    updateCompression() {
1494        if (!UI.rfb) return;
1495
1496        UI.rfb.compressionLevel = parseInt(UI.getSetting('compression'));
1497    },
1498
1499/* ------^-------
1500 *  /COMPRESSION
1501 * ==============
1502 *    KEYBOARD
1503 * ------v------*/
1504
1505    showVirtualKeyboard() {
1506        if (!isTouchDevice) return;
1507
1508        const input = document.getElementById('noVNC_keyboardinput');
1509
1510        if (document.activeElement == input) return;
1511
1512        input.focus();
1513
1514        try {
1515            const l = input.value.length;
1516            // Move the caret to the end
1517            input.setSelectionRange(l, l);
1518        } catch (err) {
1519            // setSelectionRange is undefined in Google Chrome
1520        }
1521    },
1522
1523    hideVirtualKeyboard() {
1524        if (!isTouchDevice) return;
1525
1526        const input = document.getElementById('noVNC_keyboardinput');
1527
1528        if (document.activeElement != input) return;
1529
1530        input.blur();
1531    },
1532
1533    toggleVirtualKeyboard() {
1534        if (document.getElementById('noVNC_keyboard_button')
1535            .classList.contains("noVNC_selected")) {
1536            UI.hideVirtualKeyboard();
1537        } else {
1538            UI.showVirtualKeyboard();
1539        }
1540    },
1541
1542    onfocusVirtualKeyboard(event) {
1543        document.getElementById('noVNC_keyboard_button')
1544            .classList.add("noVNC_selected");
1545        if (UI.rfb) {
1546            UI.rfb.focusOnClick = false;
1547        }
1548    },
1549
1550    onblurVirtualKeyboard(event) {
1551        document.getElementById('noVNC_keyboard_button')
1552            .classList.remove("noVNC_selected");
1553        if (UI.rfb) {
1554            UI.rfb.focusOnClick = true;
1555        }
1556    },
1557
1558    keepVirtualKeyboard(event) {
1559        const input = document.getElementById('noVNC_keyboardinput');
1560
1561        // Only prevent focus change if the virtual keyboard is active
1562        if (document.activeElement != input) {
1563            return;
1564        }
1565
1566        // Only allow focus to move to other elements that need
1567        // focus to function properly
1568        if (event.target.form !== undefined) {
1569            switch (event.target.type) {
1570                case 'text':
1571                case 'email':
1572                case 'search':
1573                case 'password':
1574                case 'tel':
1575                case 'url':
1576                case 'textarea':
1577                case 'select-one':
1578                case 'select-multiple':
1579                    return;
1580            }
1581        }
1582
1583        event.preventDefault();
1584    },
1585
1586    keyboardinputReset() {
1587        const kbi = document.getElementById('noVNC_keyboardinput');
1588        kbi.value = new Array(UI.defaultKeyboardinputLen).join("_");
1589        UI.lastKeyboardinput = kbi.value;
1590    },
1591
1592    keyEvent(keysym, code, down) {
1593        if (!UI.rfb) return;
1594
1595        UI.rfb.sendKey(keysym, code, down);
1596    },
1597
1598    // When normal keyboard events are left uncought, use the input events from
1599    // the keyboardinput element instead and generate the corresponding key events.
1600    // This code is required since some browsers on Android are inconsistent in
1601    // sending keyCodes in the normal keyboard events when using on screen keyboards.
1602    keyInput(event) {
1603
1604        if (!UI.rfb) return;
1605
1606        const newValue = event.target.value;
1607
1608        if (!UI.lastKeyboardinput) {
1609            UI.keyboardinputReset();
1610        }
1611        const oldValue = UI.lastKeyboardinput;
1612
1613        let newLen;
1614        try {
1615            // Try to check caret position since whitespace at the end
1616            // will not be considered by value.length in some browsers
1617            newLen = Math.max(event.target.selectionStart, newValue.length);
1618        } catch (err) {
1619            // selectionStart is undefined in Google Chrome
1620            newLen = newValue.length;
1621        }
1622        const oldLen = oldValue.length;
1623
1624        let inputs = newLen - oldLen;
1625        let backspaces = inputs < 0 ? -inputs : 0;
1626
1627        // Compare the old string with the new to account for
1628        // text-corrections or other input that modify existing text
1629        for (let i = 0; i < Math.min(oldLen, newLen); i++) {
1630            if (newValue.charAt(i) != oldValue.charAt(i)) {
1631                inputs = newLen - i;
1632                backspaces = oldLen - i;
1633                break;
1634            }
1635        }
1636
1637        // Send the key events
1638        for (let i = 0; i < backspaces; i++) {
1639            UI.rfb.sendKey(KeyTable.XK_BackSpace, "Backspace");
1640        }
1641        for (let i = newLen - inputs; i < newLen; i++) {
1642            UI.rfb.sendKey(keysyms.lookup(newValue.charCodeAt(i)));
1643        }
1644
1645        // Control the text content length in the keyboardinput element
1646        if (newLen > 2 * UI.defaultKeyboardinputLen) {
1647            UI.keyboardinputReset();
1648        } else if (newLen < 1) {
1649            // There always have to be some text in the keyboardinput
1650            // element with which backspace can interact.
1651            UI.keyboardinputReset();
1652            // This sometimes causes the keyboard to disappear for a second
1653            // but it is required for the android keyboard to recognize that
1654            // text has been added to the field
1655            event.target.blur();
1656            // This has to be ran outside of the input handler in order to work
1657            setTimeout(event.target.focus.bind(event.target), 0);
1658        } else {
1659            UI.lastKeyboardinput = newValue;
1660        }
1661    },
1662
1663/* ------^-------
1664 *   /KEYBOARD
1665 * ==============
1666 *   EXTRA KEYS
1667 * ------v------*/
1668
1669    openExtraKeys() {
1670        UI.closeAllPanels();
1671        UI.openControlbar();
1672
1673        document.getElementById('noVNC_modifiers')
1674            .classList.add("noVNC_open");
1675        document.getElementById('noVNC_toggle_extra_keys_button')
1676            .classList.add("noVNC_selected");
1677    },
1678
1679    closeExtraKeys() {
1680        document.getElementById('noVNC_modifiers')
1681            .classList.remove("noVNC_open");
1682        document.getElementById('noVNC_toggle_extra_keys_button')
1683            .classList.remove("noVNC_selected");
1684    },
1685
1686    toggleExtraKeys() {
1687        if (document.getElementById('noVNC_modifiers')
1688            .classList.contains("noVNC_open")) {
1689            UI.closeExtraKeys();
1690        } else  {
1691            UI.openExtraKeys();
1692        }
1693    },
1694
1695    sendEsc() {
1696        UI.sendKey(KeyTable.XK_Escape, "Escape");
1697    },
1698
1699    sendTab() {
1700        UI.sendKey(KeyTable.XK_Tab, "Tab");
1701    },
1702
1703    toggleCtrl() {
1704        const btn = document.getElementById('noVNC_toggle_ctrl_button');
1705        if (btn.classList.contains("noVNC_selected")) {
1706            UI.sendKey(KeyTable.XK_Control_L, "ControlLeft", false);
1707            btn.classList.remove("noVNC_selected");
1708        } else {
1709            UI.sendKey(KeyTable.XK_Control_L, "ControlLeft", true);
1710            btn.classList.add("noVNC_selected");
1711        }
1712    },
1713
1714    toggleWindows() {
1715        const btn = document.getElementById('noVNC_toggle_windows_button');
1716        if (btn.classList.contains("noVNC_selected")) {
1717            UI.sendKey(KeyTable.XK_Super_L, "MetaLeft", false);
1718            btn.classList.remove("noVNC_selected");
1719        } else {
1720            UI.sendKey(KeyTable.XK_Super_L, "MetaLeft", true);
1721            btn.classList.add("noVNC_selected");
1722        }
1723    },
1724
1725    toggleAlt() {
1726        const btn = document.getElementById('noVNC_toggle_alt_button');
1727        if (btn.classList.contains("noVNC_selected")) {
1728            UI.sendKey(KeyTable.XK_Alt_L, "AltLeft", false);
1729            btn.classList.remove("noVNC_selected");
1730        } else {
1731            UI.sendKey(KeyTable.XK_Alt_L, "AltLeft", true);
1732            btn.classList.add("noVNC_selected");
1733        }
1734    },
1735
1736    sendCtrlAltDel() {
1737        UI.rfb.sendCtrlAltDel();
1738        // See below
1739        UI.rfb.focus();
1740        UI.idleControlbar();
1741    },
1742
1743    sendKey(keysym, code, down) {
1744        UI.rfb.sendKey(keysym, code, down);
1745
1746        // Move focus to the screen in order to be able to use the
1747        // keyboard right after these extra keys.
1748        // The exception is when a virtual keyboard is used, because
1749        // if we focus the screen the virtual keyboard would be closed.
1750        // In this case we focus our special virtual keyboard input
1751        // element instead.
1752        if (document.getElementById('noVNC_keyboard_button')
1753            .classList.contains("noVNC_selected")) {
1754            document.getElementById('noVNC_keyboardinput').focus();
1755        } else {
1756            UI.rfb.focus();
1757        }
1758        // fade out the controlbar to highlight that
1759        // the focus has been moved to the screen
1760        UI.idleControlbar();
1761    },
1762
1763/* ------^-------
1764 *   /EXTRA KEYS
1765 * ==============
1766 *     MISC
1767 * ------v------*/
1768
1769    updateViewOnly() {
1770        if (!UI.rfb) return;
1771        UI.rfb.viewOnly = UI.getSetting('view_only');
1772
1773        UI.updateBeforeUnload();
1774
1775        // Hide input related buttons in view only mode
1776        if (UI.rfb.viewOnly) {
1777            document.getElementById('noVNC_keyboard_button')
1778                .classList.add('noVNC_hidden');
1779            document.getElementById('noVNC_toggle_extra_keys_button')
1780                .classList.add('noVNC_hidden');
1781            document.getElementById('noVNC_clipboard_button')
1782                .classList.add('noVNC_hidden');
1783        } else {
1784            document.getElementById('noVNC_keyboard_button')
1785                .classList.remove('noVNC_hidden');
1786            document.getElementById('noVNC_toggle_extra_keys_button')
1787                .classList.remove('noVNC_hidden');
1788            document.getElementById('noVNC_clipboard_button')
1789                .classList.remove('noVNC_hidden');
1790        }
1791    },
1792
1793    updateClipboard() {
1794        browserAsyncClipboardSupport()
1795            .then((support) => {
1796                if (support === 'unsupported') {
1797                    // Use fallback clipboard panel
1798                    return;
1799                }
1800                if (support === 'denied' || support === 'available') {
1801                    UI.closeClipboardPanel();
1802                    document.getElementById('noVNC_clipboard_button')
1803                        .classList.add('noVNC_hidden');
1804                    document.getElementById('noVNC_clipboard_button')
1805                        .removeEventListener('click', UI.toggleClipboardPanel);
1806                    document.getElementById('noVNC_clipboard_text')
1807                        .removeEventListener('change', UI.clipboardSend);
1808                    if (UI.rfb) {
1809                        UI.rfb.removeEventListener('clipboard', UI.clipboardReceive);
1810                    }
1811                }
1812            })
1813            .catch(() => {
1814                // Treat as unsupported
1815            });
1816    },
1817
1818    updateShowDotCursor() {
1819        if (!UI.rfb) return;
1820        UI.rfb.showDotCursor = UI.getSetting('show_dot');
1821    },
1822
1823    updateLogging() {
1824        WebUtil.initLogging(UI.getSetting('logging'));
1825    },
1826
1827    updateDesktopName(e) {
1828        UI.desktopName = e.detail.name;
1829        // Display the desktop name in the document title
1830        document.title = e.detail.name + " - " + PAGE_TITLE;
1831    },
1832
1833    updateRequestWakelock() {
1834        if (!UI.rfb) return;
1835        if (UI.getSetting('keep_device_awake')) {
1836            UI.wakeLockManager.acquire();
1837        } else {
1838            UI.wakeLockManager.release();
1839        }
1840    },
1841
1842
1843    bell(e) {
1844        if (UI.getSetting('bell') === 'on') {
1845            const promise = document.getElementById('noVNC_bell').play();
1846            // The standards disagree on the return value here
1847            if (promise) {
1848                promise.catch((e) => {
1849                    if (e.name === "NotAllowedError") {
1850                        // Ignore when the browser doesn't let us play audio.
1851                        // It is common that the browsers require audio to be
1852                        // initiated from a user action.
1853                    } else {
1854                        Log.Error("Unable to play bell: " + e);
1855                    }
1856                });
1857            }
1858        }
1859    },
1860
1861    //Helper to add options to dropdown.
1862    addOption(selectbox, text, value) {
1863        const optn = document.createElement("OPTION");
1864        optn.text = text;
1865        optn.value = value;
1866        selectbox.options.add(optn);
1867    },
1868
1869/* ------^-------
1870 *    /MISC
1871 * ==============
1872 */
1873};
1874
1875export default UI;