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