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