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