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