]> git.proxmox.com Git - mirror_novnc.git/blob - app/ui.js
Move error handler into separate file
[mirror_novnc.git] / app / ui.js
1 /*
2 * noVNC: HTML5 VNC client
3 * Copyright (C) 2012 Joel Martin
4 * Copyright (C) 2016 Samuel Mannehed for Cendio AB
5 * Copyright (C) 2016 Pierre Ossman for Cendio AB
6 * Licensed under MPL 2.0 (see LICENSE.txt)
7 *
8 * See README.md for usage and integration instructions.
9 */
10
11 /* jslint white: false, browser: true */
12 /* global window, document.getElementById, Util, WebUtil, RFB, Display */
13
14 import * as Log from '../core/util/logging.js';
15 import _, { l10n } from '../core/util/localization.js'
16 import { isTouchDevice, browserSupportsCursorURIs as cursorURIsSupported } from '../core/util/browsers.js';
17 import { setCapture, getPointerEvent } from '../core/util/events.js';
18 import KeyTable from "../core/input/keysym.js";
19 import keysyms from "../core/input/keysymdef.js";
20 import RFB from "../core/rfb.js";
21 import Display from "../core/display.js";
22 import * as WebUtil from "./webutil.js";
23
24 const 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 },
56
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 },
62
63 // Render default UI and initialize settings menu
64 start: function(callback) {
65
66 // Setup global variables first
67 UI.isSafari = (navigator.userAgent.indexOf('Safari') !== -1 &&
68 navigator.userAgent.indexOf('Chrome') === -1);
69
70 UI.initSettings();
71
72 // Translate the DOM
73 l10n.translateDOM();
74
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 }
81
82 // Restore control bar position
83 if (WebUtil.readSetting('controlbar_pos') === 'right') {
84 UI.toggleControlbarSide();
85 }
86
87 UI.initFullscreen();
88
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);
100
101 UI.openControlbar();
102
103 // Show the connect panel on first load unless autoconnecting
104 if (!autoconnect) {
105 UI.openConnectPanel();
106 }
107
108 UI.updateViewClip();
109
110 UI.updateVisualState();
111
112 document.getElementById('noVNC_setting_host').focus();
113 document.documentElement.classList.remove("noVNC_loading");
114
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 }
122
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 },
141
142 initSettings: function() {
143 var i;
144
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 }
150
151 // Settings with immediate effects
152 UI.initSetting('logging', 'warn');
153 UI.updateLogging();
154
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;
161 }
162 else if (window.location.protocol.substring(0,4) == 'http') {
163 port = 80;
164 }
165 }
166
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;
199 }
200 }
201 }
202 }
203 },
204
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 },
226
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 },
415
416 /* ------^-------
417 * /EVENT HANDLERS
418 * ==============
419 * VISUAL
420 * ------v------*/
421
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 }
460
461 UI.updateVisualState();
462 },
463
464 // Disable/enable controls depending on connection state
465 updateVisualState: function() {
466 //Log.Debug(">> updateVisualState");
467
468 UI.enableDisableViewClip();
469
470 if (cursorURIsSupported() && !isTouchDevice) {
471 UI.enableSetting('cursor');
472 } else {
473 UI.disableSetting('cursor');
474 }
475
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 }
500
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 }
513
514 // State change disables viewport dragging.
515 // It is enabled (toggled) by direct click on the button
516 UI.setViewDrag(false);
517
518 // State change also closes the password dialog
519 document.getElementById('noVNC_password_dlg')
520 .classList.remove('noVNC_open');
521
522 //Log.Debug("<< updateVisualState");
523 },
524
525 showStatus: function(text, status_type, time) {
526 var statusElem = document.getElementById('noVNC_status');
527
528 clearTimeout(UI.statusTimeout);
529
530 if (typeof status_type === 'undefined') {
531 status_type = 'normal';
532 }
533
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 }
552
553 statusElem.textContent = text;
554 statusElem.classList.add("noVNC_open");
555
556 // If no time was specified, show the status for 1.5 seconds
557 if (typeof time === 'undefined') {
558 time = 1500;
559 }
560
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 }
628
629 // Consider this a movement of the handle
630 UI.controlbarDrag = true;
631 },
632
633 dragControlbarHandle: function (e) {
634 if (!UI.controlbarGrabbed) return;
635
636 var ptr = getPointerEvent(e);
637
638 var anchor = document.getElementById('noVNC_control_bar_anchor');
639 if (ptr.clientX < (window.innerWidth * 0.1)) {
640 if (anchor.classList.contains("noVNC_right")) {
641 UI.toggleControlbarSide();
642 }
643 } else if (ptr.clientX > (window.innerWidth * 0.9)) {
644 if (!anchor.classList.contains("noVNC_right")) {
645 UI.toggleControlbarSide();
646 }
647 }
648
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);
654
655 if (dragDistance < dragThreshold) return;
656
657 UI.controlbarDrag = true;
658 }
659
660 var eventY = ptr.clientY - UI.controlbarMouseDownOffsetY;
661
662 UI.moveControlbarHandle(eventY);
663
664 e.preventDefault();
665 e.stopPropagation();
666 UI.keepControlbar();
667 UI.activateControlbar();
668 },
669
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;
677
678 // These heights need to be non-zero for the below logic to work
679 if (handleHeight === 0 || controlbarBounds.height === 0) {
680 return;
681 }
682
683 var newY = viewportRelativeY;
684
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;
689
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 }
696
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 }
702
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();
722 e.preventDefault();
723 e.stopPropagation();
724 UI.keepControlbar();
725 UI.activateControlbar();
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 },
757
758 /* ------^-------
759 * /VISUAL
760 * ==============
761 * SETTINGS
762 * ------v------*/
763
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 },
774
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) {
778
779 // Save the cookie for this session
780 if (typeof value !== 'undefined') {
781 WebUtil.writeSetting(name, value);
782 }
783
784 // Update the settings control
785 value = UI.getSetting(name);
786
787 var ctrl = document.getElementById('noVNC_setting_' + name);
788 if (ctrl.type === 'checkbox') {
789 ctrl.checked = value;
790
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;
796 }
797 }
798 } else {
799 /*Weird IE9 error leads to 'null' appearring
800 in textboxes instead of ''.*/
801 if (value === null) {
802 value = "";
803 }
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;
832 }
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 },
851
852 /* ------^-------
853 * /SETTINGS
854 * ==============
855 * PANELS
856 * ------v------*/
857
858 closeAllPanels: function() {
859 UI.closeSettingsPanel();
860 UI.closeXvpPanel();
861 UI.closeClipboardPanel();
862 UI.closeExtraKeys();
863 },
864
865 /* ------^-------
866 * /PANELS
867 * ==============
868 * SETTINGS (panel)
869 * ------v------*/
870
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 },
915
916 /* ------^-------
917 * /SETTINGS
918 * ==============
919 * XVP
920 * ------v------*/
921
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 },
947
948 // Disable/enable XVP button
949 updateXvpButton: function(ver) {
950 if (ver >= 1 && !UI.rfb.get_view_only()) {
951 document.getElementById('noVNC_xvp_button')
952 .classList.remove("noVNC_hidden");
953 } else {
954 document.getElementById('noVNC_xvp_button')
955 .classList.add("noVNC_hidden");
956 // Close XVP panel if open
957 UI.closeXvpPanel();
958 }
959 },
960
961 /* ------^-------
962 * /XVP
963 * ==============
964 * CLIPBOARD
965 * ------v------*/
966
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 },
1010
1011 /* ------^-------
1012 * /CLIPBOARD
1013 * ==============
1014 * CONNECTION
1015 * ------v------*/
1016
1017 openConnectPanel: function() {
1018 document.getElementById('noVNC_connect_dlg')
1019 .classList.add("noVNC_open");
1020 },
1021
1022 closeConnectPanel: function() {
1023 document.getElementById('noVNC_connect_dlg')
1024 .classList.remove("noVNC_open");
1025 },
1026
1027 connect: function(event, password) {
1028 var host = UI.getSetting('host');
1029 var port = UI.getSetting('port');
1030 var path = UI.getSetting('path');
1031
1032 if (typeof password === 'undefined') {
1033 password = WebUtil.getConfigVar('password');
1034 }
1035
1036 if (password === null) {
1037 password = undefined;
1038 }
1039
1040 if ((!host) || (!port)) {
1041 var msg = _("Must set host and port");
1042 Log.Error(msg);
1043 UI.showStatus(msg, 'error');
1044 return;
1045 }
1046
1047 if (!UI.initRFB()) return;
1048
1049 UI.closeAllPanels();
1050 UI.closeConnectPanel();
1051
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'));
1056
1057 UI.updateLocalCursor();
1058 UI.updateViewOnly();
1059
1060 UI.rfb.connect(host, port, password, path);
1061 },
1062
1063 disconnect: function() {
1064 UI.closeAllPanels();
1065 UI.rfb.disconnect();
1066
1067 // Disable automatic reconnecting
1068 UI.inhibit_reconnect = true;
1069
1070 // Restore the callback used for initial resize
1071 UI.rfb.set_onFBUComplete(UI.initialResize);
1072
1073 // Don't display the connection settings until we're actually disconnected
1074 },
1075
1076 reconnect: function() {
1077 UI.reconnect_callback = null;
1078
1079 // if reconnect has been disabled in the meantime, do nothing.
1080 if (UI.inhibit_reconnect) {
1081 return;
1082 }
1083
1084 UI.connect(null, UI.reconnect_password);
1085 },
1086
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");
1093
1094 var delay = parseInt(UI.getSetting('reconnect_delay'));
1095 UI.reconnect_callback = setTimeout(UI.reconnect, delay);
1096 return;
1097 }
1098
1099 UI.openControlbar();
1100 UI.openConnectPanel();
1101 },
1102
1103 cancelReconnect: function() {
1104 if (UI.reconnect_callback !== null) {
1105 clearTimeout(UI.reconnect_callback);
1106 UI.reconnect_callback = null;
1107 }
1108
1109 document.documentElement.classList.remove("noVNC_reconnecting");
1110 UI.openControlbar();
1111 UI.openConnectPanel();
1112 },
1113
1114 /* ------^-------
1115 * /CONNECTION
1116 * ==============
1117 * PASSWORD
1118 * ------v------*/
1119
1120 passwordRequired: function(rfb, msg) {
1121
1122 document.getElementById('noVNC_password_dlg')
1123 .classList.add('noVNC_open');
1124
1125 setTimeout(function () {
1126 document.getElementById('noVNC_password_input').focus();
1127 }, 100);
1128
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 },
1145
1146 /* ------^-------
1147 * /PASSWORD
1148 * ==============
1149 * FULLSCREEN
1150 * ------v------*/
1151
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();
1175 }
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 },
1193
1194 /* ------^-------
1195 * /FULLSCREEN
1196 * ==============
1197 * RESIZE
1198 * ------v------*/
1199
1200 // Apply remote resizing or local scaling
1201 applyResizeMode: function() {
1202 if (!UI.rfb) return;
1203
1204 var screen = UI.screenSize();
1205
1206 if (screen && UI.connected && UI.rfb.get_display()) {
1207
1208 var display = UI.rfb.get_display();
1209 var resizeMode = UI.getSetting('resize');
1210 display.set_scale(1);
1211
1212 // Make sure the viewport is adjusted first
1213 UI.updateViewClip();
1214
1215 if (resizeMode === 'remote') {
1216
1217 // Request changing the resolution of the remote display to
1218 // the size of the local browser viewport.
1219
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);
1230
1231 } else if (resizeMode === 'scale' || resizeMode === 'downscale') {
1232 var downscaleOnly = resizeMode === 'downscale';
1233 display.autoscale(screen.w, screen.h, downscaleOnly);
1234 UI.fixScrollbars();
1235 }
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 },
1256
1257 /* ------^-------
1258 * /RESIZE
1259 * ==============
1260 * CLIPPING
1261 * ------v------*/
1262
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 }
1285
1286 if (cur_clip !== new_clip) {
1287 display.set_viewport(new_clip);
1288 }
1289
1290 var size = UI.screenSize();
1291
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 }
1298
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 },
1315
1316 /* ------^-------
1317 * /CLIPPING
1318 * ==============
1319 * VIEWDRAG
1320 * ------v------*/
1321
1322 toggleViewDrag: function() {
1323 if (!UI.rfb) return;
1324
1325 var drag = UI.rfb.get_viewportDrag();
1326 UI.setViewDrag(!drag);
1327 },
1328
1329 // Set the view drag mode which moves the viewport on mouse drags
1330 setViewDrag: function(drag) {
1331 if (!UI.rfb) return;
1332
1333 UI.rfb.set_viewportDrag(drag);
1334
1335 UI.updateViewDrag();
1336 },
1337
1338 updateViewDrag: function() {
1339 var clipping = false;
1340
1341 if (!UI.connected) return;
1342
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 }
1349
1350 var viewDragButton = document.getElementById('noVNC_view_drag_button');
1351
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");
1370
1371 if (clipping) {
1372 viewDragButton.disabled = false;
1373 } else {
1374 viewDragButton.disabled = true;
1375 }
1376 } else {
1377 viewDragButton.disabled = false;
1378
1379 if (clipping) {
1380 viewDragButton.classList.remove("noVNC_hidden");
1381 } else {
1382 viewDragButton.classList.add("noVNC_hidden");
1383 }
1384 }
1385 },
1386
1387 /* ------^-------
1388 * /VIEWDRAG
1389 * ==============
1390 * KEYBOARD
1391 * ------v------*/
1392
1393 showVirtualKeyboard: function() {
1394 if (!isTouchDevice) return;
1395
1396 var input = document.getElementById('noVNC_keyboardinput');
1397
1398 if (document.activeElement == input) return;
1399
1400 input.focus();
1401
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 },
1408
1409 hideVirtualKeyboard: function() {
1410 if (!isTouchDevice) return;
1411
1412 var input = document.getElementById('noVNC_keyboardinput');
1413
1414 if (document.activeElement != input) return;
1415
1416 input.blur();
1417 },
1418
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 },
1427
1428 onfocusVirtualKeyboard: function(event) {
1429 document.getElementById('noVNC_keyboard_button')
1430 .classList.add("noVNC_selected");
1431 },
1432
1433 onblurVirtualKeyboard: function(event) {
1434 document.getElementById('noVNC_keyboard_button')
1435 .classList.remove("noVNC_selected");
1436 },
1437
1438 keepVirtualKeyboard: function(event) {
1439 var input = document.getElementById('noVNC_keyboardinput');
1440
1441 // Only prevent focus change if the virtual keyboard is active
1442 if (document.activeElement != input) {
1443 return;
1444 }
1445
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;
1460 }
1461 }
1462
1463 event.preventDefault();
1464 },
1465
1466 keyboardinputReset: function() {
1467 var kbi = document.getElementById('noVNC_keyboardinput');
1468 kbi.value = new Array(UI.defaultKeyboardinputLen).join("_");
1469 UI.lastKeyboardinput = kbi.value;
1470 },
1471
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) {
1477
1478 if (!UI.rfb) return;
1479
1480 var newValue = event.target.value;
1481
1482 if (!UI.lastKeyboardinput) {
1483 UI.keyboardinputReset();
1484 }
1485 var oldValue = UI.lastKeyboardinput;
1486
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 }
1505
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;
1514 }
1515 }
1516
1517 // Send the key events
1518 for (i = 0; i < backspaces; i++) {
1519 UI.rfb.sendKey(KeyTable.XK_BackSpace);
1520 }
1521 for (i = newLen - inputs; i < newLen; i++) {
1522 UI.rfb.sendKey(keysyms.fromUnicode(newValue.charCodeAt(i)).keysym);
1523 }
1524
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 },
1542
1543 /* ------^-------
1544 * /KEYBOARD
1545 * ==============
1546 * EXTRA KEYS
1547 * ------v------*/
1548
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() {
1576 UI.rfb.sendKey(KeyTable.XK_Escape);
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")) {
1586 UI.rfb.sendKey(KeyTable.XK_Control_L, false);
1587 btn.classList.remove("noVNC_selected");
1588 } else {
1589 UI.rfb.sendKey(KeyTable.XK_Control_L, true);
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")) {
1597 UI.rfb.sendKey(KeyTable.XK_Alt_L, false);
1598 btn.classList.remove("noVNC_selected");
1599 } else {
1600 UI.rfb.sendKey(KeyTable.XK_Alt_L, true);
1601 btn.classList.add("noVNC_selected");
1602 }
1603 },
1604
1605 sendCtrlAltDel: function() {
1606 UI.rfb.sendCtrlAltDel();
1607 },
1608
1609 /* ------^-------
1610 * /EXTRA KEYS
1611 * ==============
1612 * MISC
1613 * ------v------*/
1614
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 }
1620
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");
1629 }
1630 }
1631 },
1632
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 },
1639
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() {
1648 if (!UI.rfb) return;
1649 UI.rfb.set_local_cursor(UI.getSetting('cursor'));
1650 },
1651
1652 updateViewOnly: function() {
1653 if (!UI.rfb) return;
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 },
1689
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 },
1697
1698 /* ------^-------
1699 * /MISC
1700 * ==============
1701 */
1702 };
1703
1704 // Set up translations
1705 var LINGUAS = ["de", "el", "nl", "sv"];
1706 l10n.setup(LINGUAS);
1707 if (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) {
1714 throw err;
1715 });
1716 } else {
1717 UI.prime();
1718 }
1719
1720 export default UI;