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