main
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;