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