]> git.proxmox.com Git - mirror_novnc.git/blame - app/ui.js
feat: add French localization strings
[mirror_novnc.git] / app / ui.js
CommitLineData
15046f00
JM
1/*
2 * noVNC: HTML5 VNC client
412d9306 3 * Copyright (C) 2019 The noVNC Authors
1d728ace 4 * Licensed under MPL 2.0 (see LICENSE.txt)
15046f00
JM
5 *
6 * See README.md for usage and integration instructions.
7 */
43cf7bd8 8
6d6f0db0 9import * as Log from '../core/util/logging.js';
7279364c 10import _, { l10n } from './localization.js';
e52a278e 11import { isTouchDevice, isSafari, hasScrollbarGutter, dragThreshold }
ef64917a 12 from '../core/util/browser.js';
6d6f0db0 13import { setCapture, getPointerEvent } from '../core/util/events.js';
3ae0bb09
SR
14import KeyTable from "../core/input/keysym.js";
15import keysyms from "../core/input/keysymdef.js";
867daa98 16import Keyboard from "../core/input/keyboard.js";
3ae0bb09 17import RFB from "../core/rfb.js";
6d6f0db0 18import * as WebUtil from "./webutil.js";
bbbf42bb 19
c90245da
NL
20const PAGE_TITLE = "noVNC";
21
2b5f94fa 22const UI = {
6d6f0db0
SR
23
24 connected: false,
25 desktopName: "",
26
6d6f0db0
SR
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
6d6f0db0
SR
37 lastKeyboardinput: null,
38 defaultKeyboardinputLen: 100,
39
756af5b4
SM
40 inhibitReconnect: true,
41 reconnectCallback: null,
42 reconnectPassword: null,
6d6f0db0 43
1c9b904d
JD
44 prime() {
45 return WebUtil.initSettings().then(() => {
46 if (document.readyState === "interactive" || document.readyState === "complete") {
47 return UI.start();
48 }
529c64e1 49
1c9b904d
JD
50 return new Promise((resolve, reject) => {
51 document.addEventListener('DOMContentLoaded', () => UI.start().then(resolve).catch(reject));
52 });
53 });
6d6f0db0 54 },
529c64e1 55
6d6f0db0 56 // Render default UI and initialize settings menu
1c9b904d 57 start() {
044d54ed 58
6d6f0db0 59 UI.initSettings();
0f6af1e3 60
6d6f0db0
SR
61 // Translate the DOM
62 l10n.translateDOM();
0f6af1e3 63
6cd9bacf
PO
64 fetch('./package.json')
65 .then((response) => {
66 if (!response.ok) {
67 throw Error("" + response.status + " " + response.statusText);
68 }
69 return response.json();
70 })
15c7b7a6
JD
71 .then((packageInfo) => {
72 Array.from(document.getElementsByClassName('noVNC_version')).forEach(el => el.innerText = packageInfo.version);
73 })
897b465b 74 .catch((err) => {
15c7b7a6
JD
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
6d6f0db0
SR
81 // Adapt the interface for touch screen devices
82 if (isTouchDevice) {
83 document.documentElement.classList.add("noVNC_touch");
84 // Remove the address bar
651c23ec 85 setTimeout(() => window.scrollTo(0, 1), 100);
6d6f0db0 86 }
0f6af1e3 87
6d6f0db0
SR
88 // Restore control bar position
89 if (WebUtil.readSetting('controlbar_pos') === 'right') {
90 UI.toggleControlbarSide();
91 }
edffd9e2 92
6d6f0db0 93 UI.initFullscreen();
0f6af1e3 94
6d6f0db0 95 // Setup event handlers
6d6f0db0
SR
96 UI.addControlbarHandlers();
97 UI.addTouchSpecificHandlers();
98 UI.addExtraKeysHandlers();
cd523e8f 99 UI.addMachineHandlers();
6d6f0db0
SR
100 UI.addConnectionControlHandlers();
101 UI.addClipboardHandlers();
102 UI.addSettingsHandlers();
103 document.getElementById("noVNC_status")
104 .addEventListener('click', UI.hideStatus);
cf348b78 105
55988e7a
PO
106 // Bootstrap fallback input handler
107 UI.keyboardinputReset();
108
6d6f0db0 109 UI.openControlbar();
b3d91b78 110
ee5cae9f 111 UI.updateVisualState('init');
b3c932c3 112
e25f9c40 113 document.documentElement.classList.remove("noVNC_loading");
0f6af1e3 114
2b5f94fa 115 let autoconnect = WebUtil.getConfigVar('autoconnect', false);
6d6f0db0
SR
116 if (autoconnect === 'true' || autoconnect == '1') {
117 autoconnect = true;
118 UI.connect();
119 } else {
120 autoconnect = false;
067accb8
GK
121 // Show the connect panel on first load unless autoconnecting
122 UI.openConnectPanel();
6d6f0db0 123 }
0f6af1e3 124
1c9b904d 125 return Promise.resolve(UI.rfb);
6d6f0db0
SR
126 },
127
0e4808bf 128 initFullscreen() {
6d6f0db0
SR
129 // Only show the button if fullscreen is properly supported
130 // * Safari doesn't support alphanumerical input while in fullscreen
47b3eac8 131 if (!isSafari() &&
6d6f0db0
SR
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 },
0f6af1e3 141
0e4808bf 142 initSettings() {
6d6f0db0 143 // Logging selection dropdown
2b5f94fa
JD
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]);
6d6f0db0 147 }
b3d91b78 148
6d6f0db0
SR
149 // Settings with immediate effects
150 UI.initSetting('logging', 'warn');
151 UI.updateLogging();
bbbf42bb 152
6d6f0db0
SR
153 // if port == 80 (or 443) then it won't be present and should be
154 // set manually
2b5f94fa 155 let port = window.location.port;
6d6f0db0 156 if (!port) {
6786fd87 157 if (window.location.protocol.substring(0, 5) == 'https') {
6d6f0db0 158 port = 443;
6786fd87 159 } else if (window.location.protocol.substring(0, 4) == 'http') {
6d6f0db0 160 port = 80;
bbbf42bb 161 }
6d6f0db0 162 }
bbbf42bb 163
6d6f0db0
SR
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:"));
11715f20 168 UI.initSetting('view_clip', false);
6d6f0db0 169 UI.initSetting('resize', 'off');
5243cbf6 170 UI.initSetting('quality', 6);
479d8cef 171 UI.initSetting('compression', 2);
6d6f0db0
SR
172 UI.initSetting('shared', true);
173 UI.initSetting('view_only', false);
4c38179d 174 UI.initSetting('show_dot', false);
6d6f0db0
SR
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
0e4808bf 183 setupSettingLabels() {
2b5f94fa
JD
184 const labels = document.getElementsByTagName('LABEL');
185 for (let i = 0; i < labels.length; i++) {
186 const htmlFor = labels[i].htmlFor;
6d6f0db0 187 if (htmlFor != '') {
2b5f94fa 188 const elem = document.getElementById(htmlFor);
6d6f0db0
SR
189 if (elem) elem.label = labels[i];
190 } else {
191 // If 'for' isn't set, use the first input element child
2b5f94fa
JD
192 const children = labels[i].children;
193 for (let j = 0; j < children.length; j++) {
6d6f0db0
SR
194 if (children[j].form !== undefined) {
195 children[j].label = labels[i];
196 break;
24584cca
SM
197 }
198 }
199 }
6d6f0db0
SR
200 }
201 },
59387b34 202
6d6f0db0
SR
203/* ------^-------
204* /INIT
205* ==============
206* EVENT HANDLERS
207* ------v------*/
208
0e4808bf 209 addControlbarHandlers() {
6d6f0db0
SR
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")
06fe4a3e 217 .addEventListener('keydown', UI.activateControlbar);
6d6f0db0
SR
218
219 document.getElementById("noVNC_control_bar")
220 .addEventListener('mousedown', UI.keepControlbar);
221 document.getElementById("noVNC_control_bar")
06fe4a3e 222 .addEventListener('keydown', UI.keepControlbar);
6d6f0db0
SR
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
2b5f94fa
JD
236 const exps = document.getElementsByClassName("noVNC_expander");
237 for (let i = 0;i < exps.length;i++) {
6d6f0db0
SR
238 exps[i].addEventListener('click', UI.toggleExpander);
239 }
240 },
241
0e4808bf 242 addTouchSpecificHandlers() {
6d6f0db0
SR
243 document.getElementById("noVNC_keyboard_button")
244 .addEventListener('click', UI.toggleVirtualKeyboard);
245
747b4623
PO
246 UI.touchKeyboard = new Keyboard(document.getElementById('noVNC_keyboardinput'));
247 UI.touchKeyboard.onkeyevent = UI.keyEvent;
867daa98 248 UI.touchKeyboard.grab();
6d6f0db0
SR
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")
651c23ec 256 .addEventListener('submit', () => false);
6d6f0db0
SR
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);
6d6f0db0
SR
281 },
282
0e4808bf 283 addExtraKeysHandlers() {
6d6f0db0
SR
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);
3e835a5d
SS
288 document.getElementById("noVNC_toggle_windows_button")
289 .addEventListener('click', UI.toggleWindows);
6d6f0db0
SR
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
0e4808bf 300 addMachineHandlers() {
cd523e8f 301 document.getElementById("noVNC_shutdown_button")
651c23ec 302 .addEventListener('click', () => UI.rfb.machineShutdown());
cd523e8f 303 document.getElementById("noVNC_reboot_button")
651c23ec 304 .addEventListener('click', () => UI.rfb.machineReboot());
cd523e8f 305 document.getElementById("noVNC_reset_button")
651c23ec 306 .addEventListener('click', () => UI.rfb.machineReset());
cd523e8f
PO
307 document.getElementById("noVNC_power_button")
308 .addEventListener('click', UI.togglePowerPanel);
6d6f0db0
SR
309 },
310
0e4808bf 311 addConnectionControlHandlers() {
6d6f0db0
SR
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
5b453ed4
CKB
319 document.getElementById("noVNC_credentials_button")
320 .addEventListener('click', UI.setCredentials);
6d6f0db0
SR
321 },
322
0e4808bf 323 addClipboardHandlers() {
6d6f0db0
SR
324 document.getElementById("noVNC_clipboard_button")
325 .addEventListener('click', UI.toggleClipboardPanel);
6d6f0db0
SR
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.
0e4808bf 334 addSettingChangeHandler(name, changeFunc) {
2b5f94fa 335 const settingElem = document.getElementById("noVNC_setting_" + name);
6d6f0db0 336 if (changeFunc === undefined) {
651c23ec 337 changeFunc = () => UI.saveSetting(name);
6d6f0db0
SR
338 }
339 settingElem.addEventListener('change', changeFunc);
340 },
341
0e4808bf 342 addSettingsHandlers() {
6d6f0db0
SR
343 document.getElementById("noVNC_settings_button")
344 .addEventListener('click', UI.toggleSettingsPanel);
345
346 UI.addSettingChangeHandler('encrypt');
6d6f0db0 347 UI.addSettingChangeHandler('resize');
6d6f0db0 348 UI.addSettingChangeHandler('resize', UI.applyResizeMode);
4ddcc753 349 UI.addSettingChangeHandler('resize', UI.updateViewClip);
5243cbf6
PO
350 UI.addSettingChangeHandler('quality');
351 UI.addSettingChangeHandler('quality', UI.updateQuality);
479d8cef
SM
352 UI.addSettingChangeHandler('compression');
353 UI.addSettingChangeHandler('compression', UI.updateCompression);
a49ade5f
SM
354 UI.addSettingChangeHandler('view_clip');
355 UI.addSettingChangeHandler('view_clip', UI.updateViewClip);
6d6f0db0
SR
356 UI.addSettingChangeHandler('shared');
357 UI.addSettingChangeHandler('view_only');
358 UI.addSettingChangeHandler('view_only', UI.updateViewOnly);
4c38179d
AP
359 UI.addSettingChangeHandler('show_dot');
360 UI.addSettingChangeHandler('show_dot', UI.updateShowDotCursor);
6d6f0db0
SR
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
0e4808bf 371 addFullscreenHandlers() {
6d6f0db0
SR
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 },
0f6af1e3 380
95dd6001 381/* ------^-------
59387b34 382 * /EVENT HANDLERS
95dd6001 383 * ==============
384 * VISUAL
385 * ------v------*/
58ded70d 386
ee5cae9f 387 // Disable/enable controls depending on connection state
0e4808bf 388 updateVisualState(state) {
6d6f0db0
SR
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
756af5b4 395 const transitionElem = document.getElementById("noVNC_transition_text");
ee5cae9f
SM
396 switch (state) {
397 case 'init':
398 break;
6d6f0db0 399 case 'connecting':
756af5b4 400 transitionElem.textContent = _("Connecting...");
6d6f0db0
SR
401 document.documentElement.classList.add("noVNC_connecting");
402 break;
403 case 'connected':
6d6f0db0 404 document.documentElement.classList.add("noVNC_connected");
6d6f0db0
SR
405 break;
406 case 'disconnecting':
756af5b4 407 transitionElem.textContent = _("Disconnecting...");
6d6f0db0
SR
408 document.documentElement.classList.add("noVNC_disconnecting");
409 break;
410 case 'disconnected':
6d6f0db0 411 break;
ee5cae9f 412 case 'reconnecting':
756af5b4 413 transitionElem.textContent = _("Reconnecting...");
ee5cae9f 414 document.documentElement.classList.add("noVNC_reconnecting");
6d6f0db0 415 break;
6d6f0db0 416 default:
ee5cae9f 417 Log.Error("Invalid visual state: " + state);
2592c243 418 UI.showStatus(_("Internal error"), 'error');
ee5cae9f 419 return;
6d6f0db0 420 }
8d710e8b 421
6d6f0db0 422 if (UI.connected) {
4ddcc753
SM
423 UI.updateViewClip();
424
6d6f0db0 425 UI.disableSetting('encrypt');
6d6f0db0
SR
426 UI.disableSetting('shared');
427 UI.disableSetting('host');
428 UI.disableSetting('port');
429 UI.disableSetting('path');
430 UI.disableSetting('repeaterID');
6d6f0db0
SR
431
432 // Hide the controlbar after 2 seconds
433 UI.closeControlbarTimeout = setTimeout(UI.closeControlbar, 2000);
434 } else {
435 UI.enableSetting('encrypt');
6d6f0db0
SR
436 UI.enableSetting('shared');
437 UI.enableSetting('host');
438 UI.enableSetting('port');
439 UI.enableSetting('path');
440 UI.enableSetting('repeaterID');
cd523e8f 441 UI.updatePowerButton();
6d6f0db0
SR
442 UI.keepControlbar();
443 }
fdedbafb 444
42e3b03f
PO
445 // State change closes dialogs as they may not be relevant
446 // anymore
447 UI.closeAllPanels();
5b453ed4 448 document.getElementById('noVNC_credentials_dlg')
6d6f0db0 449 .classList.remove('noVNC_open');
6d6f0db0 450 },
29475d77 451
756af5b4 452 showStatus(text, statusType, time) {
2b5f94fa 453 const statusElem = document.getElementById('noVNC_status');
8a7ec6ea 454
756af5b4
SM
455 if (typeof statusType === 'undefined') {
456 statusType = 'normal';
6d6f0db0 457 }
4e471b5b 458
d623a029
SM
459 // Don't overwrite more severe visible statuses and never
460 // errors. Only shows the first error.
d623a029
SM
461 if (statusElem.classList.contains("noVNC_open")) {
462 if (statusElem.classList.contains("noVNC_status_error")) {
b17f6c69
SM
463 return;
464 }
465 if (statusElem.classList.contains("noVNC_status_warn") &&
756af5b4 466 statusType === 'normal') {
b17f6c69 467 return;
d623a029 468 }
d623a029 469 }
6d6f0db0 470
afa1f8a2
SM
471 clearTimeout(UI.statusTimeout);
472
756af5b4 473 switch (statusType) {
d623a029
SM
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;
6d6f0db0
SR
479 case 'warning':
480 case 'warn':
d623a029
SM
481 statusElem.classList.remove("noVNC_status_error");
482 statusElem.classList.remove("noVNC_status_normal");
6d6f0db0
SR
483 statusElem.classList.add("noVNC_status_warn");
484 break;
6d6f0db0
SR
485 case 'normal':
486 case 'info':
487 default:
d623a029
SM
488 statusElem.classList.remove("noVNC_status_error");
489 statusElem.classList.remove("noVNC_status_warn");
6d6f0db0
SR
490 statusElem.classList.add("noVNC_status_normal");
491 break;
492 }
8d7708c8 493
6d6f0db0
SR
494 statusElem.textContent = text;
495 statusElem.classList.add("noVNC_open");
8d7708c8 496
6d6f0db0
SR
497 // If no time was specified, show the status for 1.5 seconds
498 if (typeof time === 'undefined') {
499 time = 1500;
500 }
8d7708c8 501
6d6f0db0 502 // Error messages do not timeout
756af5b4 503 if (statusType !== 'error') {
6d6f0db0
SR
504 UI.statusTimeout = window.setTimeout(UI.hideStatus, time);
505 }
506 },
507
0e4808bf 508 hideStatus() {
6d6f0db0
SR
509 clearTimeout(UI.statusTimeout);
510 document.getElementById('noVNC_status').classList.remove("noVNC_open");
511 },
512
0e4808bf 513 activateControlbar(event) {
6d6f0db0
SR
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
0e4808bf 522 idleControlbar() {
8df281cc
SM
523 // Don't fade if a child of the control bar has focus
524 if (document.getElementById('noVNC_control_bar')
e7fa686f 525 .contains(document.activeElement) && document.hasFocus()) {
8df281cc
SM
526 UI.activateControlbar();
527 return;
528 }
529
6d6f0db0
SR
530 document.getElementById('noVNC_control_bar_anchor')
531 .classList.add("noVNC_idle");
532 },
533
0e4808bf 534 keepControlbar() {
6d6f0db0
SR
535 clearTimeout(UI.closeControlbarTimeout);
536 },
537
0e4808bf 538 openControlbar() {
6d6f0db0
SR
539 document.getElementById('noVNC_control_bar')
540 .classList.add("noVNC_open");
541 },
542
0e4808bf 543 closeControlbar() {
6d6f0db0
SR
544 UI.closeAllPanels();
545 document.getElementById('noVNC_control_bar')
546 .classList.remove("noVNC_open");
a040c402 547 UI.rfb.focus();
6d6f0db0
SR
548 },
549
0e4808bf 550 toggleControlbar() {
6d6f0db0
SR
551 if (document.getElementById('noVNC_control_bar')
552 .classList.contains("noVNC_open")) {
553 UI.closeControlbar();
554 } else {
555 UI.openControlbar();
556 }
557 },
558
0e4808bf 559 toggleControlbarSide() {
06309160
SM
560 // Temporarily disable animation, if bar is displayed, to avoid weird
561 // movement. The transitionend-event will not fire when display=none.
2b5f94fa
JD
562 const bar = document.getElementById('noVNC_control_bar');
563 const barDisplayStyle = window.getComputedStyle(bar).display;
06309160
SM
564 if (barDisplayStyle !== 'none') {
565 bar.style.transitionDuration = '0s';
651c23ec 566 bar.addEventListener('transitionend', () => bar.style.transitionDuration = '');
06309160 567 }
6d6f0db0 568
2b5f94fa 569 const anchor = document.getElementById('noVNC_control_bar_anchor');
6d6f0db0
SR
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 }
ca5c74ad 577
6d6f0db0
SR
578 // Consider this a movement of the handle
579 UI.controlbarDrag = true;
580 },
4e471b5b 581
0e4808bf 582 showControlbarHint(show) {
2b5f94fa 583 const hint = document.getElementById('noVNC_control_bar_hint');
bbc1648c
SM
584 if (show) {
585 hint.classList.add("noVNC_active");
586 } else {
587 hint.classList.remove("noVNC_active");
588 }
589 },
590
0e4808bf 591 dragControlbarHandle(e) {
6d6f0db0 592 if (!UI.controlbarGrabbed) return;
38323d4d 593
2b5f94fa 594 const ptr = getPointerEvent(e);
8ee432f1 595
2b5f94fa 596 const anchor = document.getElementById('noVNC_control_bar_anchor');
6d6f0db0 597 if (ptr.clientX < (window.innerWidth * 0.1)) {
cf348b78 598 if (anchor.classList.contains("noVNC_right")) {
6d6f0db0 599 UI.toggleControlbarSide();
8ee432f1 600 }
6d6f0db0
SR
601 } else if (ptr.clientX > (window.innerWidth * 0.9)) {
602 if (!anchor.classList.contains("noVNC_right")) {
603 UI.toggleControlbarSide();
04b399e2 604 }
6d6f0db0 605 }
04b399e2 606
6d6f0db0 607 if (!UI.controlbarDrag) {
2b5f94fa 608 const dragDistance = Math.abs(ptr.clientY - UI.controlbarMouseDownClientY);
04b399e2 609
6d6f0db0 610 if (dragDistance < dragThreshold) return;
04b399e2 611
6d6f0db0
SR
612 UI.controlbarDrag = true;
613 }
f75e4d3c 614
2b5f94fa 615 const eventY = ptr.clientY - UI.controlbarMouseDownOffsetY;
04b399e2 616
6d6f0db0 617 UI.moveControlbarHandle(eventY);
59cd99bc 618
6d6f0db0
SR
619 e.preventDefault();
620 e.stopPropagation();
621 UI.keepControlbar();
622 UI.activateControlbar();
623 },
04b399e2 624
6d6f0db0 625 // Move the handle but don't allow any position outside the bounds
0e4808bf 626 moveControlbarHandle(viewportRelativeY) {
2b5f94fa
JD
627 const handle = document.getElementById("noVNC_control_bar_handle");
628 const handleHeight = handle.getBoundingClientRect().height;
629 const controlbarBounds = document.getElementById("noVNC_control_bar")
6d6f0db0 630 .getBoundingClientRect();
2b5f94fa 631 const margin = 10;
04b399e2 632
6d6f0db0
SR
633 // These heights need to be non-zero for the below logic to work
634 if (handleHeight === 0 || controlbarBounds.height === 0) {
635 return;
636 }
04b399e2 637
2b5f94fa 638 let newY = viewportRelativeY;
04b399e2 639
6d6f0db0
SR
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;
04b399e2 644
6d6f0db0
SR
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 }
04b399e2 651
6d6f0db0
SR
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 }
04b399e2 657
6d6f0db0 658 // The transform needs coordinates that are relative to the parent
2b5f94fa 659 const parentRelativeY = newY - controlbarBounds.top;
6d6f0db0
SR
660 handle.style.transform = "translateY(" + parentRelativeY + "px)";
661 },
662
0e4808bf 663 updateControlbarHandle() {
6d6f0db0
SR
664 // Since the control bar is fixed on the viewport and not the page,
665 // the move function expects coordinates relative the the viewport.
2b5f94fa
JD
666 const handle = document.getElementById("noVNC_control_bar_handle");
667 const handleBounds = handle.getBoundingClientRect();
6d6f0db0
SR
668 UI.moveControlbarHandle(handleBounds.top);
669 },
670
0e4808bf 671 controlbarHandleMouseUp(e) {
6d6f0db0
SR
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();
04b399e2
SM
677 e.preventDefault();
678 e.stopPropagation();
de315d62
PO
679 UI.keepControlbar();
680 UI.activateControlbar();
6d6f0db0
SR
681 }
682 UI.controlbarGrabbed = false;
bbc1648c 683 UI.showControlbarHint(false);
6d6f0db0
SR
684 },
685
0e4808bf 686 controlbarHandleMouseDown(e) {
6d6f0db0
SR
687 if ((e.type == "mousedown") && (e.button != 0)) return;
688
2b5f94fa 689 const ptr = getPointerEvent(e);
6d6f0db0 690
2b5f94fa
JD
691 const handle = document.getElementById("noVNC_control_bar_handle");
692 const bounds = handle.getBoundingClientRect();
6d6f0db0 693
333ad45c
SM
694 // Touch events have implicit capture
695 if (e.type === "mousedown") {
696 setCapture(handle);
697 }
698
6d6f0db0
SR
699 UI.controlbarGrabbed = true;
700 UI.controlbarDrag = false;
701
bbc1648c
SM
702 UI.showControlbarHint(true);
703
6d6f0db0
SR
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
0e4808bf 712 toggleExpander(e) {
6d6f0db0
SR
713 if (this.classList.contains("noVNC_open")) {
714 this.classList.remove("noVNC_open");
715 } else {
716 this.classList.add("noVNC_open");
717 }
718 },
575f6983 719
95dd6001 720/* ------^-------
721 * /VISUAL
722 * ==============
723 * SETTINGS
724 * ------v------*/
725
6d6f0db0 726 // Initial page load read/initialization of settings
0e4808bf 727 initSetting(name, defVal) {
6d6f0db0 728 // Check Query string followed by cookie
2b5f94fa 729 let val = WebUtil.getConfigVar(name);
6d6f0db0
SR
730 if (val === null) {
731 val = WebUtil.readSetting(name, defVal);
732 }
8ad8f15c
AW
733 WebUtil.setSetting(name, val);
734 UI.updateSetting(name);
6d6f0db0
SR
735 return val;
736 },
bbbf42bb 737
11715f20 738 // Set the new value, update and disable form control setting
0e4808bf 739 forceSetting(name, val) {
11715f20
SM
740 WebUtil.setSetting(name, val);
741 UI.updateSetting(name);
742 UI.disableSetting(name);
743 },
744
6d6f0db0
SR
745 // Update cookie and form control setting. If value is not set, then
746 // updates from control to current cookie setting.
0e4808bf 747 updateSetting(name) {
bbbf42bb 748
6d6f0db0 749 // Update the settings control
2b5f94fa 750 let value = UI.getSetting(name);
bbbf42bb 751
2b5f94fa 752 const ctrl = document.getElementById('noVNC_setting_' + name);
6d6f0db0
SR
753 if (ctrl.type === 'checkbox') {
754 ctrl.checked = value;
bbbf42bb 755
6d6f0db0 756 } else if (typeof ctrl.options !== 'undefined') {
2b5f94fa 757 for (let i = 0; i < ctrl.options.length; i += 1) {
6d6f0db0
SR
758 if (ctrl.options[i].value === value) {
759 ctrl.selectedIndex = i;
760 break;
bbbf42bb 761 }
bbbf42bb 762 }
6d6f0db0 763 } else {
6d6f0db0
SR
764 ctrl.value = value;
765 }
766 },
767
768 // Save control setting to cookie
0e4808bf 769 saveSetting(name) {
2b5f94fa
JD
770 const ctrl = document.getElementById('noVNC_setting_' + name);
771 let val;
6d6f0db0
SR
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
0e4808bf 785 getSetting(name) {
2b5f94fa
JD
786 const ctrl = document.getElementById('noVNC_setting_' + name);
787 let val = WebUtil.readSetting(name);
6d6f0db0 788 if (typeof val !== 'undefined' && val !== null && ctrl.type === 'checkbox') {
942a3127 789 if (val.toString().toLowerCase() in {'0': 1, 'no': 1, 'false': 1}) {
6d6f0db0
SR
790 val = false;
791 } else {
792 val = true;
bbbf42bb 793 }
6d6f0db0
SR
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.
0e4808bf 801 disableSetting(name) {
2b5f94fa 802 const ctrl = document.getElementById('noVNC_setting_' + name);
6d6f0db0
SR
803 ctrl.disabled = true;
804 ctrl.label.classList.add('noVNC_disabled');
805 },
806
0e4808bf 807 enableSetting(name) {
2b5f94fa 808 const ctrl = document.getElementById('noVNC_setting_' + name);
6d6f0db0
SR
809 ctrl.disabled = false;
810 ctrl.label.classList.remove('noVNC_disabled');
811 },
24584cca 812
ed8cbe4e
PO
813/* ------^-------
814 * /SETTINGS
815 * ==============
816 * PANELS
817 * ------v------*/
818
0e4808bf 819 closeAllPanels() {
6d6f0db0 820 UI.closeSettingsPanel();
cd523e8f 821 UI.closePowerPanel();
6d6f0db0
SR
822 UI.closeClipboardPanel();
823 UI.closeExtraKeys();
824 },
ed8cbe4e
PO
825
826/* ------^-------
827 * /PANELS
828 * ==============
829 * SETTINGS (panel)
830 * ------v------*/
831
0e4808bf 832 openSettingsPanel() {
6d6f0db0
SR
833 UI.closeAllPanels();
834 UI.openControlbar();
835
836 // Refresh UI elements from saved cookies
837 UI.updateSetting('encrypt');
a49ade5f 838 UI.updateSetting('view_clip');
6d6f0db0 839 UI.updateSetting('resize');
5243cbf6 840 UI.updateSetting('quality');
479d8cef 841 UI.updateSetting('compression');
6d6f0db0
SR
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
0e4808bf 856 closeSettingsPanel() {
6d6f0db0
SR
857 document.getElementById('noVNC_settings')
858 .classList.remove("noVNC_open");
859 document.getElementById('noVNC_settings_button')
860 .classList.remove("noVNC_selected");
861 },
862
0e4808bf 863 toggleSettingsPanel() {
6d6f0db0
SR
864 if (document.getElementById('noVNC_settings')
865 .classList.contains("noVNC_open")) {
866 UI.closeSettingsPanel();
867 } else {
868 UI.openSettingsPanel();
869 }
870 },
bbbf42bb 871
95dd6001 872/* ------^-------
873 * /SETTINGS
874 * ==============
cd523e8f 875 * POWER
95dd6001 876 * ------v------*/
877
0e4808bf 878 openPowerPanel() {
6d6f0db0
SR
879 UI.closeAllPanels();
880 UI.openControlbar();
881
cd523e8f 882 document.getElementById('noVNC_power')
6d6f0db0 883 .classList.add("noVNC_open");
cd523e8f 884 document.getElementById('noVNC_power_button')
6d6f0db0
SR
885 .classList.add("noVNC_selected");
886 },
887
0e4808bf 888 closePowerPanel() {
cd523e8f 889 document.getElementById('noVNC_power')
6d6f0db0 890 .classList.remove("noVNC_open");
cd523e8f 891 document.getElementById('noVNC_power_button')
6d6f0db0
SR
892 .classList.remove("noVNC_selected");
893 },
894
0e4808bf 895 togglePowerPanel() {
cd523e8f 896 if (document.getElementById('noVNC_power')
6d6f0db0 897 .classList.contains("noVNC_open")) {
cd523e8f 898 UI.closePowerPanel();
6d6f0db0 899 } else {
cd523e8f 900 UI.openPowerPanel();
6d6f0db0
SR
901 }
902 },
ed8cbe4e 903
cd523e8f 904 // Disable/enable power button
0e4808bf 905 updatePowerButton() {
cd523e8f 906 if (UI.connected &&
747b4623
PO
907 UI.rfb.capabilities.power &&
908 !UI.rfb.viewOnly) {
cd523e8f 909 document.getElementById('noVNC_power_button')
6d6f0db0
SR
910 .classList.remove("noVNC_hidden");
911 } else {
cd523e8f 912 document.getElementById('noVNC_power_button')
6d6f0db0 913 .classList.add("noVNC_hidden");
cd523e8f
PO
914 // Close power panel if open
915 UI.closePowerPanel();
6d6f0db0
SR
916 }
917 },
bbbf42bb 918
95dd6001 919/* ------^-------
cd523e8f 920 * /POWER
95dd6001 921 * ==============
922 * CLIPBOARD
923 * ------v------*/
f8b399d7 924
0e4808bf 925 openClipboardPanel() {
6d6f0db0
SR
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
0e4808bf 935 closeClipboardPanel() {
6d6f0db0
SR
936 document.getElementById('noVNC_clipboard')
937 .classList.remove("noVNC_open");
938 document.getElementById('noVNC_clipboard_button')
939 .classList.remove("noVNC_selected");
940 },
941
0e4808bf 942 toggleClipboardPanel() {
6d6f0db0
SR
943 if (document.getElementById('noVNC_clipboard')
944 .classList.contains("noVNC_open")) {
945 UI.closeClipboardPanel();
946 } else {
947 UI.openClipboardPanel();
948 }
949 },
950
0e4808bf 951 clipboardReceive(e) {
6786fd87 952 Log.Debug(">> UI.clipboardReceive: " + e.detail.text.substr(0, 40) + "...");
e89eef94 953 document.getElementById('noVNC_clipboard_text').value = e.detail.text;
6d6f0db0
SR
954 Log.Debug("<< UI.clipboardReceive");
955 },
956
0e4808bf 957 clipboardClear() {
6d6f0db0
SR
958 document.getElementById('noVNC_clipboard_text').value = "";
959 UI.rfb.clipboardPasteFrom("");
960 },
961
0e4808bf 962 clipboardSend() {
2b5f94fa 963 const text = document.getElementById('noVNC_clipboard_text').value;
6786fd87 964 Log.Debug(">> UI.clipboardSend: " + text.substr(0, 40) + "...");
6d6f0db0
SR
965 UI.rfb.clipboardPasteFrom(text);
966 Log.Debug("<< UI.clipboardSend");
967 },
95dd6001 968
969/* ------^-------
970 * /CLIPBOARD
971 * ==============
972 * CONNECTION
973 * ------v------*/
974
0e4808bf 975 openConnectPanel() {
6d6f0db0
SR
976 document.getElementById('noVNC_connect_dlg')
977 .classList.add("noVNC_open");
978 },
b3c932c3 979
0e4808bf 980 closeConnectPanel() {
6d6f0db0
SR
981 document.getElementById('noVNC_connect_dlg')
982 .classList.remove("noVNC_open");
983 },
b3c932c3 984
0e4808bf 985 connect(event, password) {
a4822c3a
SM
986
987 // Ignore when rfb already exists
988 if (typeof UI.rfb !== 'undefined') {
989 return;
990 }
991
2b5f94fa
JD
992 const host = UI.getSetting('host');
993 const port = UI.getSetting('port');
994 const path = UI.getSetting('path');
c55f05f6 995
6d6f0db0
SR
996 if (typeof password === 'undefined') {
997 password = WebUtil.getConfigVar('password');
756af5b4 998 UI.reconnectPassword = password;
6d6f0db0 999 }
044d54ed 1000
6d6f0db0
SR
1001 if (password === null) {
1002 password = undefined;
1003 }
512d3605 1004
d623a029
SM
1005 UI.hideStatus();
1006
d593483e 1007 if (!host) {
8317524c
SM
1008 Log.Error("Can't connect when host is: " + host);
1009 UI.showStatus(_("Must set host"), 'error');
6d6f0db0
SR
1010 return;
1011 }
53fc7392 1012
6d6f0db0 1013 UI.closeConnectPanel();
4102b71c 1014
f3763a01
SM
1015 UI.updateVisualState('connecting');
1016
2b5f94fa 1017 let url;
5b4e5d01
PO
1018
1019 url = UI.getSetting('encrypt') ? 'wss' : 'ws';
1020
1021 url += '://' + host;
35068204 1022 if (port) {
5b4e5d01
PO
1023 url += ':' + port;
1024 }
1025 url += '/' + path;
1026
9b84f516 1027 UI.rfb = new RFB(document.getElementById('noVNC_container'), url,
2f4516f2
PO
1028 { shared: UI.getSetting('shared'),
1029 repeaterID: UI.getSetting('repeaterID'),
1030 credentials: { password: password } });
ee5cae9f 1031 UI.rfb.addEventListener("connect", UI.connectFinished);
e89eef94
PO
1032 UI.rfb.addEventListener("disconnect", UI.disconnectFinished);
1033 UI.rfb.addEventListener("credentialsrequired", UI.credentials);
d472f3f1 1034 UI.rfb.addEventListener("securityfailure", UI.securityFailed);
651c23ec 1035 UI.rfb.addEventListener("capabilities", UI.updatePowerButton);
e89eef94
PO
1036 UI.rfb.addEventListener("clipboard", UI.clipboardReceive);
1037 UI.rfb.addEventListener("bell", UI.bell);
e89eef94 1038 UI.rfb.addEventListener("desktopname", UI.updateDesktopName);
9b84f516
PO
1039 UI.rfb.clipViewport = UI.getSetting('view_clip');
1040 UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale';
1041 UI.rfb.resizeSession = UI.getSetting('resize') === 'remote';
5243cbf6 1042 UI.rfb.qualityLevel = parseInt(UI.getSetting('quality'));
479d8cef 1043 UI.rfb.compressionLevel = parseInt(UI.getSetting('compression'));
6aed0b4d 1044 UI.rfb.showDotCursor = UI.getSetting('show_dot');
37c60935 1045
f3763a01 1046 UI.updateViewOnly(); // requires UI.rfb
6d6f0db0 1047 },
5299db1a 1048
0e4808bf 1049 disconnect() {
6d6f0db0 1050 UI.rfb.disconnect();
8e0f0088 1051
ee5cae9f
SM
1052 UI.connected = false;
1053
6d6f0db0 1054 // Disable automatic reconnecting
756af5b4 1055 UI.inhibitReconnect = true;
044d54ed 1056
ee5cae9f
SM
1057 UI.updateVisualState('disconnecting');
1058
6d6f0db0
SR
1059 // Don't display the connection settings until we're actually disconnected
1060 },
bbbf42bb 1061
0e4808bf 1062 reconnect() {
756af5b4 1063 UI.reconnectCallback = null;
044d54ed 1064
6d6f0db0 1065 // if reconnect has been disabled in the meantime, do nothing.
756af5b4 1066 if (UI.inhibitReconnect) {
6d6f0db0
SR
1067 return;
1068 }
044d54ed 1069
756af5b4 1070 UI.connect(null, UI.reconnectPassword);
6d6f0db0 1071 },
044d54ed 1072
0e4808bf 1073 cancelReconnect() {
756af5b4
SM
1074 if (UI.reconnectCallback !== null) {
1075 clearTimeout(UI.reconnectCallback);
1076 UI.reconnectCallback = null;
68958038
SM
1077 }
1078
1079 UI.updateVisualState('disconnected');
1080
1081 UI.openControlbar();
1082 UI.openConnectPanel();
1083 },
1084
0e4808bf 1085 connectFinished(e) {
ee5cae9f 1086 UI.connected = true;
756af5b4 1087 UI.inhibitReconnect = false;
ee5cae9f
SM
1088
1089 let msg;
c995c086
SM
1090 if (UI.getSetting('encrypt')) {
1091 msg = _("Connected (encrypted) to ") + UI.desktopName;
ee5cae9f 1092 } else {
c995c086 1093 msg = _("Connected (unencrypted) to ") + UI.desktopName;
ee5cae9f
SM
1094 }
1095 UI.showStatus(msg);
1096 UI.updateVisualState('connected');
1097
1098 // Do this last because it can only be used on rendered elements
9b84f516 1099 UI.rfb.focus();
ee5cae9f
SM
1100 },
1101
0e4808bf 1102 disconnectFinished(e) {
2b5f94fa 1103 const wasConnected = UI.connected;
7f1049c0 1104
ee5cae9f
SM
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
d1aeb435
PO
1111 UI.rfb = undefined;
1112
d472f3f1 1113 if (!e.detail.clean) {
ee5cae9f 1114 UI.updateVisualState('disconnected');
7f1049c0
SM
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 }
756af5b4 1121 } else if (UI.getSetting('reconnect', false) === true && !UI.inhibitReconnect) {
ee5cae9f 1122 UI.updateVisualState('reconnecting');
044d54ed 1123
2b5f94fa 1124 const delay = parseInt(UI.getSetting('reconnect_delay'));
756af5b4 1125 UI.reconnectCallback = setTimeout(UI.reconnect, delay);
6d6f0db0 1126 return;
ee5cae9f
SM
1127 } else {
1128 UI.updateVisualState('disconnected');
1129 UI.showStatus(_("Disconnected"), 'normal');
6d6f0db0 1130 }
044d54ed 1131
c90245da
NL
1132 document.title = PAGE_TITLE;
1133
6d6f0db0
SR
1134 UI.openControlbar();
1135 UI.openConnectPanel();
1136 },
044d54ed 1137
0e4808bf 1138 securityFailed(e) {
d472f3f1
SM
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");
6d6f0db0 1148 }
d472f3f1 1149 UI.showStatus(msg, 'error');
6d6f0db0 1150 },
3bb12056 1151
7d714b15
SM
1152/* ------^-------
1153 * /CONNECTION
1154 * ==============
1155 * PASSWORD
1156 * ------v------*/
1157
0e4808bf 1158 credentials(e) {
430f00d6 1159 // FIXME: handle more types
5b453ed4
CKB
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";
b8d1a8bb 1165 if (e.detail.types.indexOf("username") === -1) {
5b453ed4
CKB
1166 document.getElementById("noVNC_username_block").classList.add("noVNC_hidden");
1167 } else {
1168 inputFocus = inputFocus === "none" ? "noVNC_username_input" : inputFocus;
1169 }
b8d1a8bb 1170 if (e.detail.types.indexOf("password") === -1) {
5b453ed4
CKB
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')
6d6f0db0 1176 .classList.add('noVNC_open');
7d714b15 1177
651c23ec 1178 setTimeout(() => document
5b453ed4 1179 .getElementById(inputFocus).focus(), 100);
7d714b15 1180
5b453ed4
CKB
1181 Log.Warn("Server asked for credentials");
1182 UI.showStatus(_("Credentials are required"), "warning");
6d6f0db0
SR
1183 },
1184
5b453ed4 1185 setCredentials(e) {
a80b5fda
PO
1186 // Prevent actually submitting the form
1187 e.preventDefault();
1188
5b453ed4
CKB
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;
c23665dd 1194 // Clear the input after reading the password
5b453ed4
CKB
1195 inputElemPassword.value = "";
1196
1197 UI.rfb.sendCredentials({ username: username, password: password });
756af5b4 1198 UI.reconnectPassword = password;
5b453ed4 1199 document.getElementById('noVNC_credentials_dlg')
6d6f0db0 1200 .classList.remove('noVNC_open');
6d6f0db0 1201 },
58ded70d 1202
95dd6001 1203/* ------^-------
7d714b15 1204 * /PASSWORD
95dd6001 1205 * ==============
1206 * FULLSCREEN
1207 * ------v------*/
1208
0e4808bf 1209 toggleFullscreen() {
32e08195
SM
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();
6d6f0db0
SR
1222 }
1223 } else {
32e08195
SM
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();
7d1dc09a 1232 }
6d6f0db0 1233 }
6d6f0db0
SR
1234 UI.updateFullscreenButton();
1235 },
1236
0e4808bf 1237 updateFullscreenButton() {
6d6f0db0
SR
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 },
7d1dc09a 1249
95dd6001 1250/* ------^-------
1251 * /FULLSCREEN
1252 * ==============
1253 * RESIZE
1254 * ------v------*/
777cb7a0 1255
6d6f0db0 1256 // Apply remote resizing or local scaling
0e4808bf 1257 applyResizeMode() {
6d6f0db0 1258 if (!UI.rfb) return;
58ded70d 1259
9b84f516
PO
1260 UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale';
1261 UI.rfb.resizeSession = UI.getSetting('resize') === 'remote';
6d6f0db0 1262 },
bbbf42bb 1263
95dd6001 1264/* ------^-------
1265 * /RESIZE
1266 * ==============
a49ade5f 1267 * VIEW CLIPPING
95dd6001 1268 * ------v------*/
1269
4ddcc753
SM
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.
0e4808bf 1273 updateViewClip() {
6d6f0db0
SR
1274 if (!UI.rfb) return;
1275
4ddcc753 1276 const scaling = UI.getSetting('resize') === 'scale';
6d6f0db0 1277
4ddcc753
SM
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;
e52a278e
SM
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
4ddcc753
SM
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');
6d6f0db0 1290 }
8e0f0088 1291
6d6f0db0
SR
1292 // Changing the viewport may change the state of
1293 // the dragging button
1294 UI.updateViewDrag();
1295 },
1296
95dd6001 1297/* ------^-------
a49ade5f 1298 * /VIEW CLIPPING
95dd6001 1299 * ==============
1300 * VIEWDRAG
1301 * ------v------*/
1302
0e4808bf 1303 toggleViewDrag() {
6d6f0db0 1304 if (!UI.rfb) return;
bbbf42bb 1305
2bbd15cc 1306 UI.rfb.dragViewport = !UI.rfb.dragViewport;
6d6f0db0
SR
1307 UI.updateViewDrag();
1308 },
f0d9ab96 1309
0e4808bf 1310 updateViewDrag() {
6d6f0db0 1311 if (!UI.connected) return;
6244e383 1312
2b5f94fa 1313 const viewDragButton = document.getElementById('noVNC_view_drag_button');
29a0e6a8 1314
9b84f516
PO
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.
0460e5fd 1318 UI.rfb.dragViewport = false;
6d6f0db0
SR
1319 }
1320
0460e5fd 1321 if (UI.rfb.dragViewport) {
6d6f0db0
SR
1322 viewDragButton.classList.add("noVNC_selected");
1323 } else {
1324 viewDragButton.classList.remove("noVNC_selected");
1325 }
1326
f2d42dc3 1327 if (UI.rfb.clipViewport) {
6d6f0db0 1328 viewDragButton.classList.remove("noVNC_hidden");
6d6f0db0 1329 } else {
f2d42dc3 1330 viewDragButton.classList.add("noVNC_hidden");
6d6f0db0
SR
1331 }
1332 },
f8b399d7 1333
95dd6001 1334/* ------^-------
1335 * /VIEWDRAG
1336 * ==============
5243cbf6
PO
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 * ==============
479d8cef
SM
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 * ==============
95dd6001 1361 * KEYBOARD
1362 * ------v------*/
fdf21468 1363
0e4808bf 1364 showVirtualKeyboard() {
6d6f0db0 1365 if (!isTouchDevice) return;
4b30f9ce 1366
2b5f94fa 1367 const input = document.getElementById('noVNC_keyboardinput');
4b30f9ce 1368
6d6f0db0 1369 if (document.activeElement == input) return;
4b30f9ce 1370
6d6f0db0 1371 input.focus();
4b30f9ce 1372
6d6f0db0 1373 try {
2b5f94fa 1374 const l = input.value.length;
6d6f0db0
SR
1375 // Move the caret to the end
1376 input.setSelectionRange(l, l);
8727f598 1377 } catch (err) {
4a16dc51 1378 // setSelectionRange is undefined in Google Chrome
8727f598 1379 }
6d6f0db0 1380 },
4b30f9ce 1381
0e4808bf 1382 hideVirtualKeyboard() {
6d6f0db0 1383 if (!isTouchDevice) return;
4b30f9ce 1384
2b5f94fa 1385 const input = document.getElementById('noVNC_keyboardinput');
4b30f9ce 1386
6d6f0db0 1387 if (document.activeElement != input) return;
4b30f9ce 1388
6d6f0db0
SR
1389 input.blur();
1390 },
4b30f9ce 1391
0e4808bf 1392 toggleVirtualKeyboard() {
6d6f0db0
SR
1393 if (document.getElementById('noVNC_keyboard_button')
1394 .classList.contains("noVNC_selected")) {
1395 UI.hideVirtualKeyboard();
1396 } else {
1397 UI.showVirtualKeyboard();
1398 }
1399 },
bbbf42bb 1400
0e4808bf 1401 onfocusVirtualKeyboard(event) {
6d6f0db0
SR
1402 document.getElementById('noVNC_keyboard_button')
1403 .classList.add("noVNC_selected");
1d6ff4a3 1404 if (UI.rfb) {
b8dfb983 1405 UI.rfb.focusOnClick = false;
1d6ff4a3 1406 }
6d6f0db0 1407 },
fdf21468 1408
0e4808bf 1409 onblurVirtualKeyboard(event) {
6d6f0db0
SR
1410 document.getElementById('noVNC_keyboard_button')
1411 .classList.remove("noVNC_selected");
1d6ff4a3 1412 if (UI.rfb) {
b8dfb983 1413 UI.rfb.focusOnClick = true;
1d6ff4a3 1414 }
6d6f0db0 1415 },
ffcadf95 1416
0e4808bf 1417 keepVirtualKeyboard(event) {
2b5f94fa 1418 const input = document.getElementById('noVNC_keyboardinput');
ffcadf95 1419
6d6f0db0
SR
1420 // Only prevent focus change if the virtual keyboard is active
1421 if (document.activeElement != input) {
1422 return;
1423 }
ffcadf95 1424
6d6f0db0
SR
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;
ffcadf95 1439 }
6d6f0db0 1440 }
ffcadf95 1441
1d6ff4a3 1442 event.preventDefault();
6d6f0db0 1443 },
bbbf42bb 1444
0e4808bf 1445 keyboardinputReset() {
2b5f94fa 1446 const kbi = document.getElementById('noVNC_keyboardinput');
6d6f0db0
SR
1447 kbi.value = new Array(UI.defaultKeyboardinputLen).join("_");
1448 UI.lastKeyboardinput = kbi.value;
1449 },
bbbf42bb 1450
0e4808bf 1451 keyEvent(keysym, code, down) {
867daa98
PO
1452 if (!UI.rfb) return;
1453
1454 UI.rfb.sendKey(keysym, code, down);
1455 },
1456
6d6f0db0
SR
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.
0e4808bf 1461 keyInput(event) {
3b8ec46f 1462
6d6f0db0 1463 if (!UI.rfb) return;
3b8ec46f 1464
2b5f94fa 1465 const newValue = event.target.value;
1138bdd4 1466
6d6f0db0
SR
1467 if (!UI.lastKeyboardinput) {
1468 UI.keyboardinputReset();
1469 }
2b5f94fa 1470 const oldValue = UI.lastKeyboardinput;
bbbf42bb 1471
2b5f94fa 1472 let newLen;
6d6f0db0
SR
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 }
2b5f94fa 1481 const oldLen = oldValue.length;
6d6f0db0 1482
2b5f94fa
JD
1483 let inputs = newLen - oldLen;
1484 let backspaces = inputs < 0 ? -inputs : 0;
8e0f0088 1485
6d6f0db0
SR
1486 // Compare the old string with the new to account for
1487 // text-corrections or other input that modify existing text
2b5f94fa 1488 for (let i = 0; i < Math.min(oldLen, newLen); i++) {
6d6f0db0
SR
1489 if (newValue.charAt(i) != oldValue.charAt(i)) {
1490 inputs = newLen - i;
1491 backspaces = oldLen - i;
1492 break;
bbbf42bb 1493 }
6d6f0db0 1494 }
bbbf42bb 1495
6d6f0db0 1496 // Send the key events
2b5f94fa 1497 for (let i = 0; i < backspaces; i++) {
94f5cf05 1498 UI.rfb.sendKey(KeyTable.XK_BackSpace, "Backspace");
6d6f0db0 1499 }
2b5f94fa 1500 for (let i = newLen - inputs; i < newLen; i++) {
524d67f2 1501 UI.rfb.sendKey(keysyms.lookup(newValue.charCodeAt(i)));
6d6f0db0 1502 }
bbbf42bb 1503
6d6f0db0
SR
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 },
bbbf42bb 1521
4b30f9ce
SM
1522/* ------^-------
1523 * /KEYBOARD
1524 * ==============
1525 * EXTRA KEYS
1526 * ------v------*/
1527
0e4808bf 1528 openExtraKeys() {
6d6f0db0
SR
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
0e4808bf 1538 closeExtraKeys() {
6d6f0db0
SR
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
0e4808bf 1545 toggleExtraKeys() {
35068204 1546 if (document.getElementById('noVNC_modifiers')
6d6f0db0
SR
1547 .classList.contains("noVNC_open")) {
1548 UI.closeExtraKeys();
1549 } else {
1550 UI.openExtraKeys();
1551 }
1552 },
1553
0e4808bf 1554 sendEsc() {
a5aa8e12 1555 UI.sendKey(KeyTable.XK_Escape, "Escape");
6d6f0db0
SR
1556 },
1557
0e4808bf 1558 sendTab() {
a5aa8e12 1559 UI.sendKey(KeyTable.XK_Tab, "Tab");
6d6f0db0
SR
1560 },
1561
0e4808bf 1562 toggleCtrl() {
2b5f94fa 1563 const btn = document.getElementById('noVNC_toggle_ctrl_button');
6d6f0db0 1564 if (btn.classList.contains("noVNC_selected")) {
a5aa8e12 1565 UI.sendKey(KeyTable.XK_Control_L, "ControlLeft", false);
6d6f0db0
SR
1566 btn.classList.remove("noVNC_selected");
1567 } else {
a5aa8e12 1568 UI.sendKey(KeyTable.XK_Control_L, "ControlLeft", true);
6d6f0db0 1569 btn.classList.add("noVNC_selected");
3e835a5d
SS
1570 }
1571 },
1572
1573 toggleWindows() {
1574 const btn = document.getElementById('noVNC_toggle_windows_button');
1575 if (btn.classList.contains("noVNC_selected")) {
a5aa8e12 1576 UI.sendKey(KeyTable.XK_Super_L, "MetaLeft", false);
3e835a5d
SS
1577 btn.classList.remove("noVNC_selected");
1578 } else {
a5aa8e12 1579 UI.sendKey(KeyTable.XK_Super_L, "MetaLeft", true);
3e835a5d 1580 btn.classList.add("noVNC_selected");
6d6f0db0
SR
1581 }
1582 },
1583
0e4808bf 1584 toggleAlt() {
2b5f94fa 1585 const btn = document.getElementById('noVNC_toggle_alt_button');
6d6f0db0 1586 if (btn.classList.contains("noVNC_selected")) {
a5aa8e12 1587 UI.sendKey(KeyTable.XK_Alt_L, "AltLeft", false);
6d6f0db0
SR
1588 btn.classList.remove("noVNC_selected");
1589 } else {
a5aa8e12 1590 UI.sendKey(KeyTable.XK_Alt_L, "AltLeft", true);
6d6f0db0
SR
1591 btn.classList.add("noVNC_selected");
1592 }
1593 },
bbbf42bb 1594
0e4808bf 1595 sendCtrlAltDel() {
6d6f0db0 1596 UI.rfb.sendCtrlAltDel();
a5aa8e12
SM
1597 // See below
1598 UI.rfb.focus();
1599 UI.idleControlbar();
1600 },
1601
1602 sendKey(keysym, code, down) {
1603 UI.rfb.sendKey(keysym, code, down);
94a01b0a
SM
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 }
a5aa8e12
SM
1617 // fade out the controlbar to highlight that
1618 // the focus has been moved to the screen
1619 UI.idleControlbar();
6d6f0db0 1620 },
bbbf42bb 1621
95dd6001 1622/* ------^-------
4b30f9ce 1623 * /EXTRA KEYS
95dd6001 1624 * ==============
1625 * MISC
1626 * ------v------*/
1627
0e4808bf 1628 updateViewOnly() {
e6a8eb15 1629 if (!UI.rfb) return;
747b4623 1630 UI.rfb.viewOnly = UI.getSetting('view_only');
f3763a01
SM
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');
9253e178
NL
1638 document.getElementById('noVNC_clipboard_button')
1639 .classList.add('noVNC_hidden');
f3763a01
SM
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');
9253e178
NL
1645 document.getElementById('noVNC_clipboard_button')
1646 .classList.remove('noVNC_hidden');
f3763a01 1647 }
6d6f0db0
SR
1648 },
1649
4c38179d
AP
1650 updateShowDotCursor() {
1651 if (!UI.rfb) return;
1652 UI.rfb.showDotCursor = UI.getSetting('show_dot');
1653 },
1654
0e4808bf 1655 updateLogging() {
a7fe079f 1656 WebUtil.initLogging(UI.getSetting('logging'));
6d6f0db0
SR
1657 },
1658
0e4808bf 1659 updateDesktopName(e) {
e89eef94 1660 UI.desktopName = e.detail.name;
6d6f0db0 1661 // Display the desktop name in the document title
c90245da 1662 document.title = e.detail.name + " - " + PAGE_TITLE;
6d6f0db0
SR
1663 },
1664
0e4808bf 1665 bell(e) {
6d6f0db0 1666 if (WebUtil.getConfigVar('bell', 'on') === 'on') {
2b5f94fa 1667 const promise = document.getElementById('noVNC_bell').play();
a342ed70
SM
1668 // The standards disagree on the return value here
1669 if (promise) {
651c23ec 1670 promise.catch((e) => {
d4fc89d8
SM
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 });
a342ed70 1679 }
6d6f0db0
SR
1680 }
1681 },
63bf2ba5 1682
6d6f0db0 1683 //Helper to add options to dropdown.
0e4808bf 1684 addOption(selectbox, text, value) {
2b5f94fa 1685 const optn = document.createElement("OPTION");
6d6f0db0
SR
1686 optn.text = text;
1687 optn.value = value;
1688 selectbox.options.add(optn);
1689 },
bbbf42bb 1690
95dd6001 1691/* ------^-------
1692 * /MISC
1693 * ==============
1694 */
6d6f0db0
SR
1695};
1696
1697// Set up translations
dd20b17d 1698const LINGUAS = ["cs", "de", "el", "es", "fr", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "zh_CN", "zh_TW"];
6d6f0db0 1699l10n.setup(LINGUAS);
1c9b904d 1700if (l10n.language === "en" || l10n.dictionary !== undefined) {
6d6f0db0 1701 UI.prime();
1c9b904d 1702} else {
6cd9bacf
PO
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 })
1c9b904d
JD
1710 .then((translations) => { l10n.dictionary = translations; })
1711 .catch(err => Log.Error("Failed to load translations: " + err))
1712 .then(UI.prime);
6d6f0db0
SR
1713}
1714
3ae0bb09 1715export default UI;