]> git.proxmox.com Git - mirror_novnc.git/blame_incremental - app/ui.js
Merge branch 'docs' of https://github.com/CendioOssman/noVNC
[mirror_novnc.git] / app / ui.js
... / ...
CommitLineData
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
14import * as Log from '../core/util/logging.js';
15import _, { l10n } from './localization.js';
16import { isTouchDevice } from '../core/util/browsers.js';
17import { setCapture, getPointerEvent } from '../core/util/events.js';
18import KeyTable from "../core/input/keysym.js";
19import keysyms from "../core/input/keysymdef.js";
20import Keyboard from "../core/input/keyboard.js";
21import RFB from "../core/rfb.js";
22import Display from "../core/display.js";
23import * as WebUtil from "./webutil.js";
24
25var 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 var host = UI.getSetting('host');
993 var port = UI.getSetting('port');
994 var path = UI.getSetting('path');
995
996 if (typeof password === 'undefined') {
997 password = WebUtil.getConfigVar('password');
998 UI.reconnect_password = password;
999 }
1000
1001 if (password === null) {
1002 password = undefined;
1003 }
1004
1005 UI.hideStatus();
1006
1007 if (!host) {
1008 Log.Error("Can't connect when host is: " + host);
1009 UI.showStatus(_("Must set host"), 'error');
1010 return;
1011 }
1012
1013 UI.closeAllPanels();
1014 UI.closeConnectPanel();
1015
1016 UI.updateVisualState('connecting');
1017
1018 UI.updateViewOnly();
1019
1020 var url;
1021
1022 url = UI.getSetting('encrypt') ? 'wss' : 'ws';
1023
1024 url += '://' + host;
1025 if(port) {
1026 url += ':' + port;
1027 }
1028 url += '/' + path;
1029
1030 UI.rfb = new RFB(document.getElementById('noVNC_container'), url,
1031 { shared: UI.getSetting('shared'),
1032 repeaterID: UI.getSetting('repeaterID'),
1033 credentials: { password: password } });
1034 UI.rfb.addEventListener("connect", UI.connectFinished);
1035 UI.rfb.addEventListener("disconnect", UI.disconnectFinished);
1036 UI.rfb.addEventListener("credentialsrequired", UI.credentials);
1037 UI.rfb.addEventListener("securityfailure", UI.securityFailed);
1038 UI.rfb.addEventListener("capabilities", function () { UI.updatePowerButton(); });
1039 UI.rfb.addEventListener("clipboard", UI.clipboardReceive);
1040 UI.rfb.addEventListener("bell", UI.bell);
1041 UI.rfb.addEventListener("desktopname", UI.updateDesktopName);
1042 UI.rfb.clipViewport = UI.getSetting('view_clip');
1043 UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale';
1044 UI.rfb.resizeSession = UI.getSetting('resize') === 'remote';
1045 },
1046
1047 disconnect: function() {
1048 UI.closeAllPanels();
1049 UI.rfb.disconnect();
1050
1051 UI.connected = false;
1052
1053 // Disable automatic reconnecting
1054 UI.inhibit_reconnect = true;
1055
1056 UI.updateVisualState('disconnecting');
1057
1058 // Don't display the connection settings until we're actually disconnected
1059 },
1060
1061 reconnect: function() {
1062 UI.reconnect_callback = null;
1063
1064 // if reconnect has been disabled in the meantime, do nothing.
1065 if (UI.inhibit_reconnect) {
1066 return;
1067 }
1068
1069 UI.connect(null, UI.reconnect_password);
1070 },
1071
1072 cancelReconnect: function() {
1073 if (UI.reconnect_callback !== null) {
1074 clearTimeout(UI.reconnect_callback);
1075 UI.reconnect_callback = null;
1076 }
1077
1078 UI.updateVisualState('disconnected');
1079
1080 UI.openControlbar();
1081 UI.openConnectPanel();
1082 },
1083
1084 connectFinished: function (e) {
1085 UI.connected = true;
1086 UI.inhibit_reconnect = false;
1087
1088 let msg;
1089 if (UI.getSetting('encrypt')) {
1090 msg = _("Connected (encrypted) to ") + UI.desktopName;
1091 } else {
1092 msg = _("Connected (unencrypted) to ") + UI.desktopName;
1093 }
1094 UI.showStatus(msg);
1095 UI.updateVisualState('connected');
1096
1097 // Do this last because it can only be used on rendered elements
1098 UI.rfb.focus();
1099 },
1100
1101 disconnectFinished: function (e) {
1102 // This variable is ideally set when disconnection starts, but
1103 // when the disconnection isn't clean or if it is initiated by
1104 // the server, we need to do it here as well since
1105 // UI.disconnect() won't be used in those cases.
1106 UI.connected = false;
1107
1108 if (!e.detail.clean) {
1109 UI.updateVisualState('disconnected');
1110 UI.showStatus(_("Something went wrong, connection is closed"),
1111 'error');
1112 } else if (UI.getSetting('reconnect', false) === true && !UI.inhibit_reconnect) {
1113 UI.updateVisualState('reconnecting');
1114
1115 var delay = parseInt(UI.getSetting('reconnect_delay'));
1116 UI.reconnect_callback = setTimeout(UI.reconnect, delay);
1117 return;
1118 } else {
1119 UI.updateVisualState('disconnected');
1120 UI.showStatus(_("Disconnected"), 'normal');
1121 }
1122
1123 UI.openControlbar();
1124 UI.openConnectPanel();
1125 },
1126
1127 securityFailed: function (e) {
1128 let msg = "";
1129 // On security failures we might get a string with a reason
1130 // directly from the server. Note that we can't control if
1131 // this string is translated or not.
1132 if ('reason' in e.detail) {
1133 msg = _("New connection has been rejected with reason: ") +
1134 e.detail.reason;
1135 } else {
1136 msg = _("New connection has been rejected");
1137 }
1138 UI.showStatus(msg, 'error');
1139 },
1140
1141/* ------^-------
1142 * /CONNECTION
1143 * ==============
1144 * PASSWORD
1145 * ------v------*/
1146
1147 credentials: function(e) {
1148 // FIXME: handle more types
1149 document.getElementById('noVNC_password_dlg')
1150 .classList.add('noVNC_open');
1151
1152 setTimeout(function () {
1153 document.getElementById('noVNC_password_input').focus();
1154 }, 100);
1155
1156 Log.Warn("Server asked for a password");
1157 UI.showStatus(_("Password is required"), "warning");
1158 },
1159
1160 setPassword: function(e) {
1161 // Prevent actually submitting the form
1162 e.preventDefault();
1163
1164 var inputElem = document.getElementById('noVNC_password_input');
1165 var password = inputElem.value;
1166 // Clear the input after reading the password
1167 inputElem.value = "";
1168 UI.rfb.sendCredentials({ password: password });
1169 UI.reconnect_password = password;
1170 document.getElementById('noVNC_password_dlg')
1171 .classList.remove('noVNC_open');
1172 },
1173
1174/* ------^-------
1175 * /PASSWORD
1176 * ==============
1177 * FULLSCREEN
1178 * ------v------*/
1179
1180 toggleFullscreen: function() {
1181 if (document.fullscreenElement || // alternative standard method
1182 document.mozFullScreenElement || // currently working methods
1183 document.webkitFullscreenElement ||
1184 document.msFullscreenElement) {
1185 if (document.exitFullscreen) {
1186 document.exitFullscreen();
1187 } else if (document.mozCancelFullScreen) {
1188 document.mozCancelFullScreen();
1189 } else if (document.webkitExitFullscreen) {
1190 document.webkitExitFullscreen();
1191 } else if (document.msExitFullscreen) {
1192 document.msExitFullscreen();
1193 }
1194 } else {
1195 if (document.documentElement.requestFullscreen) {
1196 document.documentElement.requestFullscreen();
1197 } else if (document.documentElement.mozRequestFullScreen) {
1198 document.documentElement.mozRequestFullScreen();
1199 } else if (document.documentElement.webkitRequestFullscreen) {
1200 document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
1201 } else if (document.body.msRequestFullscreen) {
1202 document.body.msRequestFullscreen();
1203 }
1204 }
1205 UI.enableDisableViewClip();
1206 UI.updateFullscreenButton();
1207 },
1208
1209 updateFullscreenButton: function() {
1210 if (document.fullscreenElement || // alternative standard method
1211 document.mozFullScreenElement || // currently working methods
1212 document.webkitFullscreenElement ||
1213 document.msFullscreenElement ) {
1214 document.getElementById('noVNC_fullscreen_button')
1215 .classList.add("noVNC_selected");
1216 } else {
1217 document.getElementById('noVNC_fullscreen_button')
1218 .classList.remove("noVNC_selected");
1219 }
1220 },
1221
1222/* ------^-------
1223 * /FULLSCREEN
1224 * ==============
1225 * RESIZE
1226 * ------v------*/
1227
1228 // Apply remote resizing or local scaling
1229 applyResizeMode: function() {
1230 if (!UI.rfb) return;
1231
1232 UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale';
1233 UI.rfb.resizeSession = UI.getSetting('resize') === 'remote';
1234 },
1235
1236/* ------^-------
1237 * /RESIZE
1238 * ==============
1239 * VIEW CLIPPING
1240 * ------v------*/
1241
1242 // Update parameters that depend on the viewport clip setting
1243 updateViewClip: function() {
1244 if (!UI.rfb) return;
1245
1246 var cur_clip = UI.rfb.clipViewport;
1247 var new_clip = UI.getSetting('view_clip');
1248
1249 if (isTouchDevice) {
1250 // Touch devices usually have shit scrollbars
1251 new_clip = true;
1252 }
1253
1254 if (cur_clip !== new_clip) {
1255 UI.rfb.clipViewport = new_clip;
1256 }
1257
1258 // Changing the viewport may change the state of
1259 // the dragging button
1260 UI.updateViewDrag();
1261 },
1262
1263 // Handle special cases where viewport clipping is forced on/off or locked
1264 enableDisableViewClip: function() {
1265 var resizeSetting = UI.getSetting('resize');
1266 // Disable clipping if we are scaling, connected or on touch
1267 if (resizeSetting === 'scale' ||
1268 isTouchDevice) {
1269 UI.disableSetting('view_clip');
1270 } else {
1271 UI.enableSetting('view_clip');
1272 }
1273 },
1274
1275/* ------^-------
1276 * /VIEW CLIPPING
1277 * ==============
1278 * VIEWDRAG
1279 * ------v------*/
1280
1281 toggleViewDrag: function() {
1282 if (!UI.rfb) return;
1283
1284 var drag = UI.rfb.dragViewport;
1285 UI.setViewDrag(!drag);
1286 },
1287
1288 // Set the view drag mode which moves the viewport on mouse drags
1289 setViewDrag: function(drag) {
1290 if (!UI.rfb) return;
1291
1292 UI.rfb.dragViewport = drag;
1293
1294 UI.updateViewDrag();
1295 },
1296
1297 updateViewDrag: function() {
1298 if (!UI.connected) return;
1299
1300 var viewDragButton = document.getElementById('noVNC_view_drag_button');
1301
1302 if (!UI.rfb.clipViewport && UI.rfb.dragViewport) {
1303 // We are no longer clipping the viewport. Make sure
1304 // viewport drag isn't active when it can't be used.
1305 UI.rfb.dragViewport = false;
1306 }
1307
1308 if (UI.rfb.dragViewport) {
1309 viewDragButton.classList.add("noVNC_selected");
1310 } else {
1311 viewDragButton.classList.remove("noVNC_selected");
1312 }
1313
1314 // Different behaviour for touch vs non-touch
1315 // The button is disabled instead of hidden on touch devices
1316 if (isTouchDevice) {
1317 viewDragButton.classList.remove("noVNC_hidden");
1318
1319 if (UI.rfb.clipViewport) {
1320 viewDragButton.disabled = false;
1321 } else {
1322 viewDragButton.disabled = true;
1323 }
1324 } else {
1325 viewDragButton.disabled = false;
1326
1327 if (UI.rfb.clipViewport) {
1328 viewDragButton.classList.remove("noVNC_hidden");
1329 } else {
1330 viewDragButton.classList.add("noVNC_hidden");
1331 }
1332 }
1333 },
1334
1335/* ------^-------
1336 * /VIEWDRAG
1337 * ==============
1338 * KEYBOARD
1339 * ------v------*/
1340
1341 showVirtualKeyboard: function() {
1342 if (!isTouchDevice) return;
1343
1344 var input = document.getElementById('noVNC_keyboardinput');
1345
1346 if (document.activeElement == input) return;
1347
1348 input.focus();
1349
1350 try {
1351 var l = input.value.length;
1352 // Move the caret to the end
1353 input.setSelectionRange(l, l);
1354 } catch (err) {} // setSelectionRange is undefined in Google Chrome
1355 },
1356
1357 hideVirtualKeyboard: function() {
1358 if (!isTouchDevice) return;
1359
1360 var input = document.getElementById('noVNC_keyboardinput');
1361
1362 if (document.activeElement != input) return;
1363
1364 input.blur();
1365 },
1366
1367 toggleVirtualKeyboard: function () {
1368 if (document.getElementById('noVNC_keyboard_button')
1369 .classList.contains("noVNC_selected")) {
1370 UI.hideVirtualKeyboard();
1371 } else {
1372 UI.showVirtualKeyboard();
1373 }
1374 },
1375
1376 onfocusVirtualKeyboard: function(event) {
1377 document.getElementById('noVNC_keyboard_button')
1378 .classList.add("noVNC_selected");
1379 if (UI.rfb) {
1380 UI.rfb.focusOnClick = false;
1381 }
1382 },
1383
1384 onblurVirtualKeyboard: function(event) {
1385 document.getElementById('noVNC_keyboard_button')
1386 .classList.remove("noVNC_selected");
1387 if (UI.rfb) {
1388 UI.rfb.focusOnClick = true;
1389 }
1390 },
1391
1392 keepVirtualKeyboard: function(event) {
1393 var input = document.getElementById('noVNC_keyboardinput');
1394
1395 // Only prevent focus change if the virtual keyboard is active
1396 if (document.activeElement != input) {
1397 return;
1398 }
1399
1400 // Only allow focus to move to other elements that need
1401 // focus to function properly
1402 if (event.target.form !== undefined) {
1403 switch (event.target.type) {
1404 case 'text':
1405 case 'email':
1406 case 'search':
1407 case 'password':
1408 case 'tel':
1409 case 'url':
1410 case 'textarea':
1411 case 'select-one':
1412 case 'select-multiple':
1413 return;
1414 }
1415 }
1416
1417 event.preventDefault();
1418 },
1419
1420 keyboardinputReset: function() {
1421 var kbi = document.getElementById('noVNC_keyboardinput');
1422 kbi.value = new Array(UI.defaultKeyboardinputLen).join("_");
1423 UI.lastKeyboardinput = kbi.value;
1424 },
1425
1426 keyEvent: function (keysym, code, down) {
1427 if (!UI.rfb) return;
1428
1429 UI.rfb.sendKey(keysym, code, down);
1430 },
1431
1432 // When normal keyboard events are left uncought, use the input events from
1433 // the keyboardinput element instead and generate the corresponding key events.
1434 // This code is required since some browsers on Android are inconsistent in
1435 // sending keyCodes in the normal keyboard events when using on screen keyboards.
1436 keyInput: function(event) {
1437
1438 if (!UI.rfb) return;
1439
1440 var newValue = event.target.value;
1441
1442 if (!UI.lastKeyboardinput) {
1443 UI.keyboardinputReset();
1444 }
1445 var oldValue = UI.lastKeyboardinput;
1446
1447 var newLen;
1448 try {
1449 // Try to check caret position since whitespace at the end
1450 // will not be considered by value.length in some browsers
1451 newLen = Math.max(event.target.selectionStart, newValue.length);
1452 } catch (err) {
1453 // selectionStart is undefined in Google Chrome
1454 newLen = newValue.length;
1455 }
1456 var oldLen = oldValue.length;
1457
1458 var backspaces;
1459 var inputs = newLen - oldLen;
1460 if (inputs < 0) {
1461 backspaces = -inputs;
1462 } else {
1463 backspaces = 0;
1464 }
1465
1466 // Compare the old string with the new to account for
1467 // text-corrections or other input that modify existing text
1468 var i;
1469 for (i = 0; i < Math.min(oldLen, newLen); i++) {
1470 if (newValue.charAt(i) != oldValue.charAt(i)) {
1471 inputs = newLen - i;
1472 backspaces = oldLen - i;
1473 break;
1474 }
1475 }
1476
1477 // Send the key events
1478 for (i = 0; i < backspaces; i++) {
1479 UI.rfb.sendKey(KeyTable.XK_BackSpace, "Backspace");
1480 }
1481 for (i = newLen - inputs; i < newLen; i++) {
1482 UI.rfb.sendKey(keysyms.lookup(newValue.charCodeAt(i)));
1483 }
1484
1485 // Control the text content length in the keyboardinput element
1486 if (newLen > 2 * UI.defaultKeyboardinputLen) {
1487 UI.keyboardinputReset();
1488 } else if (newLen < 1) {
1489 // There always have to be some text in the keyboardinput
1490 // element with which backspace can interact.
1491 UI.keyboardinputReset();
1492 // This sometimes causes the keyboard to disappear for a second
1493 // but it is required for the android keyboard to recognize that
1494 // text has been added to the field
1495 event.target.blur();
1496 // This has to be ran outside of the input handler in order to work
1497 setTimeout(event.target.focus.bind(event.target), 0);
1498 } else {
1499 UI.lastKeyboardinput = newValue;
1500 }
1501 },
1502
1503/* ------^-------
1504 * /KEYBOARD
1505 * ==============
1506 * EXTRA KEYS
1507 * ------v------*/
1508
1509 openExtraKeys: function() {
1510 UI.closeAllPanels();
1511 UI.openControlbar();
1512
1513 document.getElementById('noVNC_modifiers')
1514 .classList.add("noVNC_open");
1515 document.getElementById('noVNC_toggle_extra_keys_button')
1516 .classList.add("noVNC_selected");
1517 },
1518
1519 closeExtraKeys: function() {
1520 document.getElementById('noVNC_modifiers')
1521 .classList.remove("noVNC_open");
1522 document.getElementById('noVNC_toggle_extra_keys_button')
1523 .classList.remove("noVNC_selected");
1524 },
1525
1526 toggleExtraKeys: function() {
1527 if(document.getElementById('noVNC_modifiers')
1528 .classList.contains("noVNC_open")) {
1529 UI.closeExtraKeys();
1530 } else {
1531 UI.openExtraKeys();
1532 }
1533 },
1534
1535 sendEsc: function() {
1536 UI.rfb.sendKey(KeyTable.XK_Escape, "Escape");
1537 },
1538
1539 sendTab: function() {
1540 UI.rfb.sendKey(KeyTable.XK_Tab);
1541 },
1542
1543 toggleCtrl: function() {
1544 var btn = document.getElementById('noVNC_toggle_ctrl_button');
1545 if (btn.classList.contains("noVNC_selected")) {
1546 UI.rfb.sendKey(KeyTable.XK_Control_L, "ControlLeft", false);
1547 btn.classList.remove("noVNC_selected");
1548 } else {
1549 UI.rfb.sendKey(KeyTable.XK_Control_L, "ControlLeft", true);
1550 btn.classList.add("noVNC_selected");
1551 }
1552 },
1553
1554 toggleAlt: function() {
1555 var btn = document.getElementById('noVNC_toggle_alt_button');
1556 if (btn.classList.contains("noVNC_selected")) {
1557 UI.rfb.sendKey(KeyTable.XK_Alt_L, "AltLeft", false);
1558 btn.classList.remove("noVNC_selected");
1559 } else {
1560 UI.rfb.sendKey(KeyTable.XK_Alt_L, "AltLeft", true);
1561 btn.classList.add("noVNC_selected");
1562 }
1563 },
1564
1565 sendCtrlAltDel: function() {
1566 UI.rfb.sendCtrlAltDel();
1567 },
1568
1569/* ------^-------
1570 * /EXTRA KEYS
1571 * ==============
1572 * MISC
1573 * ------v------*/
1574
1575 setMouseButton: function(num) {
1576 var view_only = UI.rfb.viewOnly;
1577 if (UI.rfb && !view_only) {
1578 UI.rfb.touchButton = num;
1579 }
1580
1581 var blist = [0, 1,2,4];
1582 for (var b = 0; b < blist.length; b++) {
1583 var button = document.getElementById('noVNC_mouse_button' +
1584 blist[b]);
1585 if (blist[b] === num && !view_only) {
1586 button.classList.remove("noVNC_hidden");
1587 } else {
1588 button.classList.add("noVNC_hidden");
1589 }
1590 }
1591 },
1592
1593 updateViewOnly: function() {
1594 if (!UI.rfb) return;
1595 UI.rfb.viewOnly = UI.getSetting('view_only');
1596 },
1597
1598 updateLogging: function() {
1599 WebUtil.init_logging(UI.getSetting('logging'));
1600 },
1601
1602 updateDesktopName: function(e) {
1603 UI.desktopName = e.detail.name;
1604 // Display the desktop name in the document title
1605 document.title = e.detail.name + " - noVNC";
1606 },
1607
1608 bell: function(e) {
1609 if (WebUtil.getConfigVar('bell', 'on') === 'on') {
1610 var promise = document.getElementById('noVNC_bell').play();
1611 // The standards disagree on the return value here
1612 if (promise) {
1613 promise.catch(function(e) {
1614 if (e.name === "NotAllowedError") {
1615 // Ignore when the browser doesn't let us play audio.
1616 // It is common that the browsers require audio to be
1617 // initiated from a user action.
1618 } else {
1619 Log.Error("Unable to play bell: " + e);
1620 }
1621 });
1622 }
1623 }
1624 },
1625
1626 //Helper to add options to dropdown.
1627 addOption: function(selectbox, text, value) {
1628 var optn = document.createElement("OPTION");
1629 optn.text = text;
1630 optn.value = value;
1631 selectbox.options.add(optn);
1632 },
1633
1634/* ------^-------
1635 * /MISC
1636 * ==============
1637 */
1638};
1639
1640// Set up translations
1641var LINGUAS = ["de", "el", "nl", "pl", "sv"];
1642l10n.setup(LINGUAS);
1643if (l10n.language !== "en" && l10n.dictionary === undefined) {
1644 WebUtil.fetchJSON('app/locale/' + l10n.language + '.json', function (translations) {
1645 l10n.dictionary = translations;
1646
1647 // wait for translations to load before loading the UI
1648 UI.prime();
1649 }, function (err) {
1650 throw err;
1651 });
1652} else {
1653 UI.prime();
1654}
1655
1656export default UI;