]> git.proxmox.com Git - mirror_novnc.git/blob - app/ui.js
Drop UI.rfb reference on all disconnects
[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 // State change disables viewport dragging.
442 // It is enabled (toggled) by direct click on the button
443 UI.setViewDrag(false);
444
445 // State change also closes the password dialog
446 document.getElementById('noVNC_password_dlg')
447 .classList.remove('noVNC_open');
448 },
449
450 showStatus: function(text, status_type, time) {
451 var statusElem = document.getElementById('noVNC_status');
452
453 clearTimeout(UI.statusTimeout);
454
455 if (typeof status_type === 'undefined') {
456 status_type = 'normal';
457 }
458
459 // Don't overwrite more severe visible statuses and never
460 // errors. Only shows the first error.
461 let visible_status_type = 'none';
462 if (statusElem.classList.contains("noVNC_open")) {
463 if (statusElem.classList.contains("noVNC_status_error")) {
464 visible_status_type = 'error';
465 } else if (statusElem.classList.contains("noVNC_status_warn")) {
466 visible_status_type = 'warn';
467 } else {
468 visible_status_type = 'normal';
469 }
470 }
471 if (visible_status_type === 'error' ||
472 (visible_status_type === 'warn' && status_type === 'normal')) {
473 return;
474 }
475
476 switch (status_type) {
477 case 'error':
478 statusElem.classList.remove("noVNC_status_warn");
479 statusElem.classList.remove("noVNC_status_normal");
480 statusElem.classList.add("noVNC_status_error");
481 break;
482 case 'warning':
483 case 'warn':
484 statusElem.classList.remove("noVNC_status_error");
485 statusElem.classList.remove("noVNC_status_normal");
486 statusElem.classList.add("noVNC_status_warn");
487 break;
488 case 'normal':
489 case 'info':
490 default:
491 statusElem.classList.remove("noVNC_status_error");
492 statusElem.classList.remove("noVNC_status_warn");
493 statusElem.classList.add("noVNC_status_normal");
494 break;
495 }
496
497 statusElem.textContent = text;
498 statusElem.classList.add("noVNC_open");
499
500 // If no time was specified, show the status for 1.5 seconds
501 if (typeof time === 'undefined') {
502 time = 1500;
503 }
504
505 // Error messages do not timeout
506 if (status_type !== 'error') {
507 UI.statusTimeout = window.setTimeout(UI.hideStatus, time);
508 }
509 },
510
511 hideStatus: function() {
512 clearTimeout(UI.statusTimeout);
513 document.getElementById('noVNC_status').classList.remove("noVNC_open");
514 },
515
516 activateControlbar: function(event) {
517 clearTimeout(UI.idleControlbarTimeout);
518 // We manipulate the anchor instead of the actual control
519 // bar in order to avoid creating new a stacking group
520 document.getElementById('noVNC_control_bar_anchor')
521 .classList.remove("noVNC_idle");
522 UI.idleControlbarTimeout = window.setTimeout(UI.idleControlbar, 2000);
523 },
524
525 idleControlbar: function() {
526 document.getElementById('noVNC_control_bar_anchor')
527 .classList.add("noVNC_idle");
528 },
529
530 keepControlbar: function() {
531 clearTimeout(UI.closeControlbarTimeout);
532 },
533
534 openControlbar: function() {
535 document.getElementById('noVNC_control_bar')
536 .classList.add("noVNC_open");
537 },
538
539 closeControlbar: function() {
540 UI.closeAllPanels();
541 document.getElementById('noVNC_control_bar')
542 .classList.remove("noVNC_open");
543 },
544
545 toggleControlbar: function() {
546 if (document.getElementById('noVNC_control_bar')
547 .classList.contains("noVNC_open")) {
548 UI.closeControlbar();
549 } else {
550 UI.openControlbar();
551 }
552 },
553
554 toggleControlbarSide: function () {
555 // Temporarily disable animation to avoid weird movement
556 var bar = document.getElementById('noVNC_control_bar');
557 bar.style.transitionDuration = '0s';
558 bar.addEventListener('transitionend', function () { this.style.transitionDuration = ""; });
559
560 var anchor = document.getElementById('noVNC_control_bar_anchor');
561 if (anchor.classList.contains("noVNC_right")) {
562 WebUtil.writeSetting('controlbar_pos', 'left');
563 anchor.classList.remove("noVNC_right");
564 } else {
565 WebUtil.writeSetting('controlbar_pos', 'right');
566 anchor.classList.add("noVNC_right");
567 }
568
569 // Consider this a movement of the handle
570 UI.controlbarDrag = true;
571 },
572
573 showControlbarHint: function (show) {
574 var hint = document.getElementById('noVNC_control_bar_hint');
575 if (show) {
576 hint.classList.add("noVNC_active");
577 } else {
578 hint.classList.remove("noVNC_active");
579 }
580 },
581
582 dragControlbarHandle: function (e) {
583 if (!UI.controlbarGrabbed) return;
584
585 var ptr = getPointerEvent(e);
586
587 var anchor = document.getElementById('noVNC_control_bar_anchor');
588 if (ptr.clientX < (window.innerWidth * 0.1)) {
589 if (anchor.classList.contains("noVNC_right")) {
590 UI.toggleControlbarSide();
591 }
592 } else if (ptr.clientX > (window.innerWidth * 0.9)) {
593 if (!anchor.classList.contains("noVNC_right")) {
594 UI.toggleControlbarSide();
595 }
596 }
597
598 if (!UI.controlbarDrag) {
599 // The goal is to trigger on a certain physical width, the
600 // devicePixelRatio brings us a bit closer but is not optimal.
601 var dragThreshold = 10 * (window.devicePixelRatio || 1);
602 var dragDistance = Math.abs(ptr.clientY - UI.controlbarMouseDownClientY);
603
604 if (dragDistance < dragThreshold) return;
605
606 UI.controlbarDrag = true;
607 }
608
609 var eventY = ptr.clientY - UI.controlbarMouseDownOffsetY;
610
611 UI.moveControlbarHandle(eventY);
612
613 e.preventDefault();
614 e.stopPropagation();
615 UI.keepControlbar();
616 UI.activateControlbar();
617 },
618
619 // Move the handle but don't allow any position outside the bounds
620 moveControlbarHandle: function (viewportRelativeY) {
621 var handle = document.getElementById("noVNC_control_bar_handle");
622 var handleHeight = handle.getBoundingClientRect().height;
623 var controlbarBounds = document.getElementById("noVNC_control_bar")
624 .getBoundingClientRect();
625 var margin = 10;
626
627 // These heights need to be non-zero for the below logic to work
628 if (handleHeight === 0 || controlbarBounds.height === 0) {
629 return;
630 }
631
632 var newY = viewportRelativeY;
633
634 // Check if the coordinates are outside the control bar
635 if (newY < controlbarBounds.top + margin) {
636 // Force coordinates to be below the top of the control bar
637 newY = controlbarBounds.top + margin;
638
639 } else if (newY > controlbarBounds.top +
640 controlbarBounds.height - handleHeight - margin) {
641 // Force coordinates to be above the bottom of the control bar
642 newY = controlbarBounds.top +
643 controlbarBounds.height - handleHeight - margin;
644 }
645
646 // Corner case: control bar too small for stable position
647 if (controlbarBounds.height < (handleHeight + margin * 2)) {
648 newY = controlbarBounds.top +
649 (controlbarBounds.height - handleHeight) / 2;
650 }
651
652 // The transform needs coordinates that are relative to the parent
653 var parentRelativeY = newY - controlbarBounds.top;
654 handle.style.transform = "translateY(" + parentRelativeY + "px)";
655 },
656
657 updateControlbarHandle: function () {
658 // Since the control bar is fixed on the viewport and not the page,
659 // the move function expects coordinates relative the the viewport.
660 var handle = document.getElementById("noVNC_control_bar_handle");
661 var handleBounds = handle.getBoundingClientRect();
662 UI.moveControlbarHandle(handleBounds.top);
663 },
664
665 controlbarHandleMouseUp: function(e) {
666 if ((e.type == "mouseup") && (e.button != 0)) return;
667
668 // mouseup and mousedown on the same place toggles the controlbar
669 if (UI.controlbarGrabbed && !UI.controlbarDrag) {
670 UI.toggleControlbar();
671 e.preventDefault();
672 e.stopPropagation();
673 UI.keepControlbar();
674 UI.activateControlbar();
675 }
676 UI.controlbarGrabbed = false;
677 UI.showControlbarHint(false);
678 },
679
680 controlbarHandleMouseDown: function(e) {
681 if ((e.type == "mousedown") && (e.button != 0)) return;
682
683 var ptr = getPointerEvent(e);
684
685 var handle = document.getElementById("noVNC_control_bar_handle");
686 var bounds = handle.getBoundingClientRect();
687
688 // Touch events have implicit capture
689 if (e.type === "mousedown") {
690 setCapture(handle);
691 }
692
693 UI.controlbarGrabbed = true;
694 UI.controlbarDrag = false;
695
696 UI.showControlbarHint(true);
697
698 UI.controlbarMouseDownClientY = ptr.clientY;
699 UI.controlbarMouseDownOffsetY = ptr.clientY - bounds.top;
700 e.preventDefault();
701 e.stopPropagation();
702 UI.keepControlbar();
703 UI.activateControlbar();
704 },
705
706 toggleExpander: function(e) {
707 if (this.classList.contains("noVNC_open")) {
708 this.classList.remove("noVNC_open");
709 } else {
710 this.classList.add("noVNC_open");
711 }
712 },
713
714 /* ------^-------
715 * /VISUAL
716 * ==============
717 * SETTINGS
718 * ------v------*/
719
720 // Initial page load read/initialization of settings
721 initSetting: function(name, defVal) {
722 // Check Query string followed by cookie
723 var val = WebUtil.getConfigVar(name);
724 if (val === null) {
725 val = WebUtil.readSetting(name, defVal);
726 }
727 UI.updateSetting(name, val);
728 return val;
729 },
730
731 // Update cookie and form control setting. If value is not set, then
732 // updates from control to current cookie setting.
733 updateSetting: function(name, value) {
734
735 // Save the cookie for this session
736 if (typeof value !== 'undefined') {
737 WebUtil.writeSetting(name, value);
738 }
739
740 // Update the settings control
741 value = UI.getSetting(name);
742
743 var ctrl = document.getElementById('noVNC_setting_' + name);
744 if (ctrl.type === 'checkbox') {
745 ctrl.checked = value;
746
747 } else if (typeof ctrl.options !== 'undefined') {
748 for (var i = 0; i < ctrl.options.length; i += 1) {
749 if (ctrl.options[i].value === value) {
750 ctrl.selectedIndex = i;
751 break;
752 }
753 }
754 } else {
755 /*Weird IE9 error leads to 'null' appearring
756 in textboxes instead of ''.*/
757 if (value === null) {
758 value = "";
759 }
760 ctrl.value = value;
761 }
762 },
763
764 // Save control setting to cookie
765 saveSetting: function(name) {
766 var val, ctrl = document.getElementById('noVNC_setting_' + name);
767 if (ctrl.type === 'checkbox') {
768 val = ctrl.checked;
769 } else if (typeof ctrl.options !== 'undefined') {
770 val = ctrl.options[ctrl.selectedIndex].value;
771 } else {
772 val = ctrl.value;
773 }
774 WebUtil.writeSetting(name, val);
775 //Log.Debug("Setting saved '" + name + "=" + val + "'");
776 return val;
777 },
778
779 // Read form control compatible setting from cookie
780 getSetting: function(name) {
781 var ctrl = document.getElementById('noVNC_setting_' + name);
782 var val = WebUtil.readSetting(name);
783 if (typeof val !== 'undefined' && val !== null && ctrl.type === 'checkbox') {
784 if (val.toString().toLowerCase() in {'0':1, 'no':1, 'false':1}) {
785 val = false;
786 } else {
787 val = true;
788 }
789 }
790 return val;
791 },
792
793 // These helpers compensate for the lack of parent-selectors and
794 // previous-sibling-selectors in CSS which are needed when we want to
795 // disable the labels that belong to disabled input elements.
796 disableSetting: function(name) {
797 var ctrl = document.getElementById('noVNC_setting_' + name);
798 ctrl.disabled = true;
799 ctrl.label.classList.add('noVNC_disabled');
800 },
801
802 enableSetting: function(name) {
803 var ctrl = document.getElementById('noVNC_setting_' + name);
804 ctrl.disabled = false;
805 ctrl.label.classList.remove('noVNC_disabled');
806 },
807
808 /* ------^-------
809 * /SETTINGS
810 * ==============
811 * PANELS
812 * ------v------*/
813
814 closeAllPanels: function() {
815 UI.closeSettingsPanel();
816 UI.closePowerPanel();
817 UI.closeClipboardPanel();
818 UI.closeExtraKeys();
819 },
820
821 /* ------^-------
822 * /PANELS
823 * ==============
824 * SETTINGS (panel)
825 * ------v------*/
826
827 openSettingsPanel: function() {
828 UI.closeAllPanels();
829 UI.openControlbar();
830
831 // Refresh UI elements from saved cookies
832 UI.updateSetting('encrypt');
833 UI.updateSetting('view_clip');
834 UI.updateSetting('resize');
835 UI.updateSetting('shared');
836 UI.updateSetting('view_only');
837 UI.updateSetting('path');
838 UI.updateSetting('repeaterID');
839 UI.updateSetting('logging');
840 UI.updateSetting('reconnect');
841 UI.updateSetting('reconnect_delay');
842
843 document.getElementById('noVNC_settings')
844 .classList.add("noVNC_open");
845 document.getElementById('noVNC_settings_button')
846 .classList.add("noVNC_selected");
847 },
848
849 closeSettingsPanel: function() {
850 document.getElementById('noVNC_settings')
851 .classList.remove("noVNC_open");
852 document.getElementById('noVNC_settings_button')
853 .classList.remove("noVNC_selected");
854 },
855
856 toggleSettingsPanel: function() {
857 if (document.getElementById('noVNC_settings')
858 .classList.contains("noVNC_open")) {
859 UI.closeSettingsPanel();
860 } else {
861 UI.openSettingsPanel();
862 }
863 },
864
865 /* ------^-------
866 * /SETTINGS
867 * ==============
868 * POWER
869 * ------v------*/
870
871 openPowerPanel: function() {
872 UI.closeAllPanels();
873 UI.openControlbar();
874
875 document.getElementById('noVNC_power')
876 .classList.add("noVNC_open");
877 document.getElementById('noVNC_power_button')
878 .classList.add("noVNC_selected");
879 },
880
881 closePowerPanel: function() {
882 document.getElementById('noVNC_power')
883 .classList.remove("noVNC_open");
884 document.getElementById('noVNC_power_button')
885 .classList.remove("noVNC_selected");
886 },
887
888 togglePowerPanel: function() {
889 if (document.getElementById('noVNC_power')
890 .classList.contains("noVNC_open")) {
891 UI.closePowerPanel();
892 } else {
893 UI.openPowerPanel();
894 }
895 },
896
897 // Disable/enable power button
898 updatePowerButton: function() {
899 if (UI.connected &&
900 UI.rfb.capabilities.power &&
901 !UI.rfb.viewOnly) {
902 document.getElementById('noVNC_power_button')
903 .classList.remove("noVNC_hidden");
904 } else {
905 document.getElementById('noVNC_power_button')
906 .classList.add("noVNC_hidden");
907 // Close power panel if open
908 UI.closePowerPanel();
909 }
910 },
911
912 /* ------^-------
913 * /POWER
914 * ==============
915 * CLIPBOARD
916 * ------v------*/
917
918 openClipboardPanel: function() {
919 UI.closeAllPanels();
920 UI.openControlbar();
921
922 document.getElementById('noVNC_clipboard')
923 .classList.add("noVNC_open");
924 document.getElementById('noVNC_clipboard_button')
925 .classList.add("noVNC_selected");
926 },
927
928 closeClipboardPanel: function() {
929 document.getElementById('noVNC_clipboard')
930 .classList.remove("noVNC_open");
931 document.getElementById('noVNC_clipboard_button')
932 .classList.remove("noVNC_selected");
933 },
934
935 toggleClipboardPanel: function() {
936 if (document.getElementById('noVNC_clipboard')
937 .classList.contains("noVNC_open")) {
938 UI.closeClipboardPanel();
939 } else {
940 UI.openClipboardPanel();
941 }
942 },
943
944 clipboardReceive: function(e) {
945 Log.Debug(">> UI.clipboardReceive: " + e.detail.text.substr(0,40) + "...");
946 document.getElementById('noVNC_clipboard_text').value = e.detail.text;
947 Log.Debug("<< UI.clipboardReceive");
948 },
949
950 clipboardClear: function() {
951 document.getElementById('noVNC_clipboard_text').value = "";
952 UI.rfb.clipboardPasteFrom("");
953 },
954
955 clipboardSend: function() {
956 var text = document.getElementById('noVNC_clipboard_text').value;
957 Log.Debug(">> UI.clipboardSend: " + text.substr(0,40) + "...");
958 UI.rfb.clipboardPasteFrom(text);
959 Log.Debug("<< UI.clipboardSend");
960 },
961
962 /* ------^-------
963 * /CLIPBOARD
964 * ==============
965 * CONNECTION
966 * ------v------*/
967
968 openConnectPanel: function() {
969 document.getElementById('noVNC_connect_dlg')
970 .classList.add("noVNC_open");
971 },
972
973 closeConnectPanel: function() {
974 document.getElementById('noVNC_connect_dlg')
975 .classList.remove("noVNC_open");
976 },
977
978 connect: function(event, password) {
979
980 // Ignore when rfb already exists
981 if (typeof UI.rfb !== 'undefined') {
982 return;
983 }
984
985 var host = UI.getSetting('host');
986 var port = UI.getSetting('port');
987 var path = UI.getSetting('path');
988
989 if (typeof password === 'undefined') {
990 password = WebUtil.getConfigVar('password');
991 UI.reconnect_password = password;
992 }
993
994 if (password === null) {
995 password = undefined;
996 }
997
998 UI.hideStatus();
999
1000 if (!host) {
1001 Log.Error("Can't connect when host is: " + host);
1002 UI.showStatus(_("Must set host"), 'error');
1003 return;
1004 }
1005
1006 UI.closeAllPanels();
1007 UI.closeConnectPanel();
1008
1009 UI.updateVisualState('connecting');
1010
1011 var url;
1012
1013 url = UI.getSetting('encrypt') ? 'wss' : 'ws';
1014
1015 url += '://' + host;
1016 if(port) {
1017 url += ':' + port;
1018 }
1019 url += '/' + path;
1020
1021 UI.rfb = new RFB(document.getElementById('noVNC_container'), url,
1022 { shared: UI.getSetting('shared'),
1023 repeaterID: UI.getSetting('repeaterID'),
1024 credentials: { password: password } });
1025 UI.rfb.addEventListener("connect", UI.connectFinished);
1026 UI.rfb.addEventListener("disconnect", UI.disconnectFinished);
1027 UI.rfb.addEventListener("credentialsrequired", UI.credentials);
1028 UI.rfb.addEventListener("securityfailure", UI.securityFailed);
1029 UI.rfb.addEventListener("capabilities", function () { UI.updatePowerButton(); });
1030 UI.rfb.addEventListener("clipboard", UI.clipboardReceive);
1031 UI.rfb.addEventListener("bell", UI.bell);
1032 UI.rfb.addEventListener("desktopname", UI.updateDesktopName);
1033 UI.rfb.clipViewport = UI.getSetting('view_clip');
1034 UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale';
1035 UI.rfb.resizeSession = UI.getSetting('resize') === 'remote';
1036
1037 UI.updateViewOnly(); // requires UI.rfb
1038 },
1039
1040 disconnect: function() {
1041 UI.closeAllPanels();
1042 UI.rfb.disconnect();
1043
1044 UI.connected = false;
1045
1046 // Disable automatic reconnecting
1047 UI.inhibit_reconnect = true;
1048
1049 UI.updateVisualState('disconnecting');
1050
1051 // Don't display the connection settings until we're actually disconnected
1052 },
1053
1054 reconnect: function() {
1055 UI.reconnect_callback = null;
1056
1057 // if reconnect has been disabled in the meantime, do nothing.
1058 if (UI.inhibit_reconnect) {
1059 return;
1060 }
1061
1062 UI.connect(null, UI.reconnect_password);
1063 },
1064
1065 cancelReconnect: function() {
1066 if (UI.reconnect_callback !== null) {
1067 clearTimeout(UI.reconnect_callback);
1068 UI.reconnect_callback = null;
1069 }
1070
1071 UI.updateVisualState('disconnected');
1072
1073 UI.openControlbar();
1074 UI.openConnectPanel();
1075 },
1076
1077 connectFinished: function (e) {
1078 UI.connected = true;
1079 UI.inhibit_reconnect = false;
1080
1081 let msg;
1082 if (UI.getSetting('encrypt')) {
1083 msg = _("Connected (encrypted) to ") + UI.desktopName;
1084 } else {
1085 msg = _("Connected (unencrypted) to ") + UI.desktopName;
1086 }
1087 UI.showStatus(msg);
1088 UI.updateVisualState('connected');
1089
1090 // Do this last because it can only be used on rendered elements
1091 UI.rfb.focus();
1092 },
1093
1094 disconnectFinished: function (e) {
1095 let wasConnected = UI.connected;
1096
1097 // This variable is ideally set when disconnection starts, but
1098 // when the disconnection isn't clean or if it is initiated by
1099 // the server, we need to do it here as well since
1100 // UI.disconnect() won't be used in those cases.
1101 UI.connected = false;
1102
1103 UI.rfb = undefined;
1104
1105 if (!e.detail.clean) {
1106 UI.updateVisualState('disconnected');
1107 if (wasConnected) {
1108 UI.showStatus(_("Something went wrong, connection is closed"),
1109 'error');
1110 } else {
1111 UI.showStatus(_("Failed to connect to server"), 'error');
1112 }
1113 } else if (UI.getSetting('reconnect', false) === true && !UI.inhibit_reconnect) {
1114 UI.updateVisualState('reconnecting');
1115
1116 var delay = parseInt(UI.getSetting('reconnect_delay'));
1117 UI.reconnect_callback = setTimeout(UI.reconnect, delay);
1118 return;
1119 } else {
1120 UI.updateVisualState('disconnected');
1121 UI.showStatus(_("Disconnected"), 'normal');
1122 }
1123
1124 UI.openControlbar();
1125 UI.openConnectPanel();
1126 },
1127
1128 securityFailed: function (e) {
1129 let msg = "";
1130 // On security failures we might get a string with a reason
1131 // directly from the server. Note that we can't control if
1132 // this string is translated or not.
1133 if ('reason' in e.detail) {
1134 msg = _("New connection has been rejected with reason: ") +
1135 e.detail.reason;
1136 } else {
1137 msg = _("New connection has been rejected");
1138 }
1139 UI.showStatus(msg, 'error');
1140 },
1141
1142 /* ------^-------
1143 * /CONNECTION
1144 * ==============
1145 * PASSWORD
1146 * ------v------*/
1147
1148 credentials: function(e) {
1149 // FIXME: handle more types
1150 document.getElementById('noVNC_password_dlg')
1151 .classList.add('noVNC_open');
1152
1153 setTimeout(function () {
1154 document.getElementById('noVNC_password_input').focus();
1155 }, 100);
1156
1157 Log.Warn("Server asked for a password");
1158 UI.showStatus(_("Password is required"), "warning");
1159 },
1160
1161 setPassword: function(e) {
1162 // Prevent actually submitting the form
1163 e.preventDefault();
1164
1165 var inputElem = document.getElementById('noVNC_password_input');
1166 var password = inputElem.value;
1167 // Clear the input after reading the password
1168 inputElem.value = "";
1169 UI.rfb.sendCredentials({ password: password });
1170 UI.reconnect_password = password;
1171 document.getElementById('noVNC_password_dlg')
1172 .classList.remove('noVNC_open');
1173 },
1174
1175 /* ------^-------
1176 * /PASSWORD
1177 * ==============
1178 * FULLSCREEN
1179 * ------v------*/
1180
1181 toggleFullscreen: function() {
1182 if (document.fullscreenElement || // alternative standard method
1183 document.mozFullScreenElement || // currently working methods
1184 document.webkitFullscreenElement ||
1185 document.msFullscreenElement) {
1186 if (document.exitFullscreen) {
1187 document.exitFullscreen();
1188 } else if (document.mozCancelFullScreen) {
1189 document.mozCancelFullScreen();
1190 } else if (document.webkitExitFullscreen) {
1191 document.webkitExitFullscreen();
1192 } else if (document.msExitFullscreen) {
1193 document.msExitFullscreen();
1194 }
1195 } else {
1196 if (document.documentElement.requestFullscreen) {
1197 document.documentElement.requestFullscreen();
1198 } else if (document.documentElement.mozRequestFullScreen) {
1199 document.documentElement.mozRequestFullScreen();
1200 } else if (document.documentElement.webkitRequestFullscreen) {
1201 document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
1202 } else if (document.body.msRequestFullscreen) {
1203 document.body.msRequestFullscreen();
1204 }
1205 }
1206 UI.enableDisableViewClip();
1207 UI.updateFullscreenButton();
1208 },
1209
1210 updateFullscreenButton: function() {
1211 if (document.fullscreenElement || // alternative standard method
1212 document.mozFullScreenElement || // currently working methods
1213 document.webkitFullscreenElement ||
1214 document.msFullscreenElement ) {
1215 document.getElementById('noVNC_fullscreen_button')
1216 .classList.add("noVNC_selected");
1217 } else {
1218 document.getElementById('noVNC_fullscreen_button')
1219 .classList.remove("noVNC_selected");
1220 }
1221 },
1222
1223 /* ------^-------
1224 * /FULLSCREEN
1225 * ==============
1226 * RESIZE
1227 * ------v------*/
1228
1229 // Apply remote resizing or local scaling
1230 applyResizeMode: function() {
1231 if (!UI.rfb) return;
1232
1233 UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale';
1234 UI.rfb.resizeSession = UI.getSetting('resize') === 'remote';
1235 },
1236
1237 /* ------^-------
1238 * /RESIZE
1239 * ==============
1240 * VIEW CLIPPING
1241 * ------v------*/
1242
1243 // Update parameters that depend on the viewport clip setting
1244 updateViewClip: function() {
1245 if (!UI.rfb) return;
1246
1247 var cur_clip = UI.rfb.clipViewport;
1248 var new_clip = UI.getSetting('view_clip');
1249
1250 if (isTouchDevice) {
1251 // Touch devices usually have shit scrollbars
1252 new_clip = true;
1253 }
1254
1255 if (cur_clip !== new_clip) {
1256 UI.rfb.clipViewport = new_clip;
1257 }
1258
1259 // Changing the viewport may change the state of
1260 // the dragging button
1261 UI.updateViewDrag();
1262 },
1263
1264 // Handle special cases where viewport clipping is forced on/off or locked
1265 enableDisableViewClip: function() {
1266 var resizeSetting = UI.getSetting('resize');
1267 // Disable clipping if we are scaling, connected or on touch
1268 if (resizeSetting === 'scale' ||
1269 isTouchDevice) {
1270 UI.disableSetting('view_clip');
1271 } else {
1272 UI.enableSetting('view_clip');
1273 }
1274 },
1275
1276 /* ------^-------
1277 * /VIEW CLIPPING
1278 * ==============
1279 * VIEWDRAG
1280 * ------v------*/
1281
1282 toggleViewDrag: function() {
1283 if (!UI.rfb) return;
1284
1285 var drag = UI.rfb.dragViewport;
1286 UI.setViewDrag(!drag);
1287 },
1288
1289 // Set the view drag mode which moves the viewport on mouse drags
1290 setViewDrag: function(drag) {
1291 if (!UI.rfb) return;
1292
1293 UI.rfb.dragViewport = drag;
1294
1295 UI.updateViewDrag();
1296 },
1297
1298 updateViewDrag: function() {
1299 if (!UI.connected) return;
1300
1301 var viewDragButton = document.getElementById('noVNC_view_drag_button');
1302
1303 if (!UI.rfb.clipViewport && UI.rfb.dragViewport) {
1304 // We are no longer clipping the viewport. Make sure
1305 // viewport drag isn't active when it can't be used.
1306 UI.rfb.dragViewport = false;
1307 }
1308
1309 if (UI.rfb.dragViewport) {
1310 viewDragButton.classList.add("noVNC_selected");
1311 } else {
1312 viewDragButton.classList.remove("noVNC_selected");
1313 }
1314
1315 // Different behaviour for touch vs non-touch
1316 // The button is disabled instead of hidden on touch devices
1317 if (isTouchDevice) {
1318 viewDragButton.classList.remove("noVNC_hidden");
1319
1320 if (UI.rfb.clipViewport) {
1321 viewDragButton.disabled = false;
1322 } else {
1323 viewDragButton.disabled = true;
1324 }
1325 } else {
1326 viewDragButton.disabled = false;
1327
1328 if (UI.rfb.clipViewport) {
1329 viewDragButton.classList.remove("noVNC_hidden");
1330 } else {
1331 viewDragButton.classList.add("noVNC_hidden");
1332 }
1333 }
1334 },
1335
1336 /* ------^-------
1337 * /VIEWDRAG
1338 * ==============
1339 * KEYBOARD
1340 * ------v------*/
1341
1342 showVirtualKeyboard: function() {
1343 if (!isTouchDevice) return;
1344
1345 var input = document.getElementById('noVNC_keyboardinput');
1346
1347 if (document.activeElement == input) return;
1348
1349 input.focus();
1350
1351 try {
1352 var l = input.value.length;
1353 // Move the caret to the end
1354 input.setSelectionRange(l, l);
1355 } catch (err) {} // setSelectionRange is undefined in Google Chrome
1356 },
1357
1358 hideVirtualKeyboard: function() {
1359 if (!isTouchDevice) return;
1360
1361 var input = document.getElementById('noVNC_keyboardinput');
1362
1363 if (document.activeElement != input) return;
1364
1365 input.blur();
1366 },
1367
1368 toggleVirtualKeyboard: function () {
1369 if (document.getElementById('noVNC_keyboard_button')
1370 .classList.contains("noVNC_selected")) {
1371 UI.hideVirtualKeyboard();
1372 } else {
1373 UI.showVirtualKeyboard();
1374 }
1375 },
1376
1377 onfocusVirtualKeyboard: function(event) {
1378 document.getElementById('noVNC_keyboard_button')
1379 .classList.add("noVNC_selected");
1380 if (UI.rfb) {
1381 UI.rfb.focusOnClick = false;
1382 }
1383 },
1384
1385 onblurVirtualKeyboard: function(event) {
1386 document.getElementById('noVNC_keyboard_button')
1387 .classList.remove("noVNC_selected");
1388 if (UI.rfb) {
1389 UI.rfb.focusOnClick = true;
1390 }
1391 },
1392
1393 keepVirtualKeyboard: function(event) {
1394 var input = document.getElementById('noVNC_keyboardinput');
1395
1396 // Only prevent focus change if the virtual keyboard is active
1397 if (document.activeElement != input) {
1398 return;
1399 }
1400
1401 // Only allow focus to move to other elements that need
1402 // focus to function properly
1403 if (event.target.form !== undefined) {
1404 switch (event.target.type) {
1405 case 'text':
1406 case 'email':
1407 case 'search':
1408 case 'password':
1409 case 'tel':
1410 case 'url':
1411 case 'textarea':
1412 case 'select-one':
1413 case 'select-multiple':
1414 return;
1415 }
1416 }
1417
1418 event.preventDefault();
1419 },
1420
1421 keyboardinputReset: function() {
1422 var kbi = document.getElementById('noVNC_keyboardinput');
1423 kbi.value = new Array(UI.defaultKeyboardinputLen).join("_");
1424 UI.lastKeyboardinput = kbi.value;
1425 },
1426
1427 keyEvent: function (keysym, code, down) {
1428 if (!UI.rfb) return;
1429
1430 UI.rfb.sendKey(keysym, code, down);
1431 },
1432
1433 // When normal keyboard events are left uncought, use the input events from
1434 // the keyboardinput element instead and generate the corresponding key events.
1435 // This code is required since some browsers on Android are inconsistent in
1436 // sending keyCodes in the normal keyboard events when using on screen keyboards.
1437 keyInput: function(event) {
1438
1439 if (!UI.rfb) return;
1440
1441 var newValue = event.target.value;
1442
1443 if (!UI.lastKeyboardinput) {
1444 UI.keyboardinputReset();
1445 }
1446 var oldValue = UI.lastKeyboardinput;
1447
1448 var newLen;
1449 try {
1450 // Try to check caret position since whitespace at the end
1451 // will not be considered by value.length in some browsers
1452 newLen = Math.max(event.target.selectionStart, newValue.length);
1453 } catch (err) {
1454 // selectionStart is undefined in Google Chrome
1455 newLen = newValue.length;
1456 }
1457 var oldLen = oldValue.length;
1458
1459 var backspaces;
1460 var inputs = newLen - oldLen;
1461 if (inputs < 0) {
1462 backspaces = -inputs;
1463 } else {
1464 backspaces = 0;
1465 }
1466
1467 // Compare the old string with the new to account for
1468 // text-corrections or other input that modify existing text
1469 var i;
1470 for (i = 0; i < Math.min(oldLen, newLen); i++) {
1471 if (newValue.charAt(i) != oldValue.charAt(i)) {
1472 inputs = newLen - i;
1473 backspaces = oldLen - i;
1474 break;
1475 }
1476 }
1477
1478 // Send the key events
1479 for (i = 0; i < backspaces; i++) {
1480 UI.rfb.sendKey(KeyTable.XK_BackSpace, "Backspace");
1481 }
1482 for (i = newLen - inputs; i < newLen; i++) {
1483 UI.rfb.sendKey(keysyms.lookup(newValue.charCodeAt(i)));
1484 }
1485
1486 // Control the text content length in the keyboardinput element
1487 if (newLen > 2 * UI.defaultKeyboardinputLen) {
1488 UI.keyboardinputReset();
1489 } else if (newLen < 1) {
1490 // There always have to be some text in the keyboardinput
1491 // element with which backspace can interact.
1492 UI.keyboardinputReset();
1493 // This sometimes causes the keyboard to disappear for a second
1494 // but it is required for the android keyboard to recognize that
1495 // text has been added to the field
1496 event.target.blur();
1497 // This has to be ran outside of the input handler in order to work
1498 setTimeout(event.target.focus.bind(event.target), 0);
1499 } else {
1500 UI.lastKeyboardinput = newValue;
1501 }
1502 },
1503
1504 /* ------^-------
1505 * /KEYBOARD
1506 * ==============
1507 * EXTRA KEYS
1508 * ------v------*/
1509
1510 openExtraKeys: function() {
1511 UI.closeAllPanels();
1512 UI.openControlbar();
1513
1514 document.getElementById('noVNC_modifiers')
1515 .classList.add("noVNC_open");
1516 document.getElementById('noVNC_toggle_extra_keys_button')
1517 .classList.add("noVNC_selected");
1518 },
1519
1520 closeExtraKeys: function() {
1521 document.getElementById('noVNC_modifiers')
1522 .classList.remove("noVNC_open");
1523 document.getElementById('noVNC_toggle_extra_keys_button')
1524 .classList.remove("noVNC_selected");
1525 },
1526
1527 toggleExtraKeys: function() {
1528 if(document.getElementById('noVNC_modifiers')
1529 .classList.contains("noVNC_open")) {
1530 UI.closeExtraKeys();
1531 } else {
1532 UI.openExtraKeys();
1533 }
1534 },
1535
1536 sendEsc: function() {
1537 UI.rfb.sendKey(KeyTable.XK_Escape, "Escape");
1538 },
1539
1540 sendTab: function() {
1541 UI.rfb.sendKey(KeyTable.XK_Tab);
1542 },
1543
1544 toggleCtrl: function() {
1545 var btn = document.getElementById('noVNC_toggle_ctrl_button');
1546 if (btn.classList.contains("noVNC_selected")) {
1547 UI.rfb.sendKey(KeyTable.XK_Control_L, "ControlLeft", false);
1548 btn.classList.remove("noVNC_selected");
1549 } else {
1550 UI.rfb.sendKey(KeyTable.XK_Control_L, "ControlLeft", true);
1551 btn.classList.add("noVNC_selected");
1552 }
1553 },
1554
1555 toggleAlt: function() {
1556 var btn = document.getElementById('noVNC_toggle_alt_button');
1557 if (btn.classList.contains("noVNC_selected")) {
1558 UI.rfb.sendKey(KeyTable.XK_Alt_L, "AltLeft", false);
1559 btn.classList.remove("noVNC_selected");
1560 } else {
1561 UI.rfb.sendKey(KeyTable.XK_Alt_L, "AltLeft", true);
1562 btn.classList.add("noVNC_selected");
1563 }
1564 },
1565
1566 sendCtrlAltDel: function() {
1567 UI.rfb.sendCtrlAltDel();
1568 },
1569
1570 /* ------^-------
1571 * /EXTRA KEYS
1572 * ==============
1573 * MISC
1574 * ------v------*/
1575
1576 setMouseButton: function(num) {
1577 var view_only = UI.rfb.viewOnly;
1578 if (UI.rfb && !view_only) {
1579 UI.rfb.touchButton = num;
1580 }
1581
1582 var blist = [0, 1,2,4];
1583 for (var b = 0; b < blist.length; b++) {
1584 var button = document.getElementById('noVNC_mouse_button' +
1585 blist[b]);
1586 if (blist[b] === num && !view_only) {
1587 button.classList.remove("noVNC_hidden");
1588 } else {
1589 button.classList.add("noVNC_hidden");
1590 }
1591 }
1592 },
1593
1594 updateViewOnly: function() {
1595 if (!UI.rfb) return;
1596 UI.rfb.viewOnly = UI.getSetting('view_only');
1597
1598 // Hide input related buttons in view only mode
1599 if (UI.rfb.viewOnly) {
1600 document.getElementById('noVNC_keyboard_button')
1601 .classList.add('noVNC_hidden');
1602 document.getElementById('noVNC_toggle_extra_keys_button')
1603 .classList.add('noVNC_hidden');
1604 } else {
1605 document.getElementById('noVNC_keyboard_button')
1606 .classList.remove('noVNC_hidden');
1607 document.getElementById('noVNC_toggle_extra_keys_button')
1608 .classList.remove('noVNC_hidden');
1609 }
1610 UI.setMouseButton(1); //has it's own logic for hiding/showing
1611 },
1612
1613 updateLogging: function() {
1614 WebUtil.init_logging(UI.getSetting('logging'));
1615 },
1616
1617 updateDesktopName: function(e) {
1618 UI.desktopName = e.detail.name;
1619 // Display the desktop name in the document title
1620 document.title = e.detail.name + " - noVNC";
1621 },
1622
1623 bell: function(e) {
1624 if (WebUtil.getConfigVar('bell', 'on') === 'on') {
1625 var promise = document.getElementById('noVNC_bell').play();
1626 // The standards disagree on the return value here
1627 if (promise) {
1628 promise.catch(function(e) {
1629 if (e.name === "NotAllowedError") {
1630 // Ignore when the browser doesn't let us play audio.
1631 // It is common that the browsers require audio to be
1632 // initiated from a user action.
1633 } else {
1634 Log.Error("Unable to play bell: " + e);
1635 }
1636 });
1637 }
1638 }
1639 },
1640
1641 //Helper to add options to dropdown.
1642 addOption: function(selectbox, text, value) {
1643 var optn = document.createElement("OPTION");
1644 optn.text = text;
1645 optn.value = value;
1646 selectbox.options.add(optn);
1647 },
1648
1649 /* ------^-------
1650 * /MISC
1651 * ==============
1652 */
1653 };
1654
1655 // Set up translations
1656 var LINGUAS = ["de", "el", "nl", "pl", "sv", "zh"];
1657 l10n.setup(LINGUAS);
1658 if (l10n.language !== "en" && l10n.dictionary === undefined) {
1659 WebUtil.fetchJSON('app/locale/' + l10n.language + '.json', function (translations) {
1660 l10n.dictionary = translations;
1661
1662 // wait for translations to load before loading the UI
1663 UI.prime();
1664 }, function (err) {
1665 throw err;
1666 });
1667 } else {
1668 UI.prime();
1669 }
1670
1671 export default UI;