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