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