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)
8 * See README.md for usage and integration instructions.
11 /* jslint white: false, browser: true */
12 /* global window, document.getElementById, Util, WebUtil, RFB, Display */
15 * import Util from "../core/util";
16 * import KeyTable from "../core/input/keysym";
17 * import RFB from "../core/rfb";
18 * import Display from "../core/display";
19 * import WebUtil from "./webutil";
27 /* [begin skip-as-module] */
28 // Load supporting scripts
30 {'core': ["base64.js", "websock.js", "des.js", "input/keysymdef.js",
31 "input/xtscancodes.js", "input/util.js", "input/devices.js",
32 "display.js", "inflator.js", "rfb.js", "input/keysym.js"]});
34 window
.onscriptsload = function () { UI
.load(); };
35 /* [end skip-as-module] */
44 hideKeyboardTimeout
: null,
45 idleControlbarTimeout
: null,
46 closeControlbarTimeout
: null,
48 controlbarGrabbed
: false,
49 controlbarDrag
: false,
50 controlbarMouseDownClientY
: 0,
51 controlbarMouseDownOffsetY
: 0,
52 keyboardVisible
: false,
55 rememberedClipSetting
: null,
56 lastKeyboardinput
: null,
57 defaultKeyboardinputLen
: 100,
59 // Setup rfb object, load settings from browser storage, then call
60 // UI.init to setup the UI/menus
61 load: function(callback
) {
62 WebUtil
.initSettings(UI
.start
, callback
);
65 // Render default UI and initialize settings menu
66 start: function(callback
) {
68 // Setup global variables first
69 UI
.isSafari
= (navigator
.userAgent
.indexOf('Safari') !== -1 &&
70 navigator
.userAgent
.indexOf('Chrome') === -1);
74 // Adapt the interface for touch screen devices
75 if (Util
.isTouchDevice
) {
76 document
.documentElement
.classList
.add("noVNC_touch");
77 // Remove the address bar
78 setTimeout(function() { window
.scrollTo(0, 1); }, 100);
79 UI
.forceSetting('clip', true);
81 UI
.initSetting('clip', false);
84 // Setup and initialize event handlers
85 UI
.setupWindowEvents();
87 UI
.addControlbarHandlers();
88 UI
.addTouchSpecificHandlers();
89 UI
.addExtraKeysHandlers();
91 UI
.addConnectionControlHandlers();
92 UI
.addClipboardHandlers();
93 UI
.addSettingsHandlers();
95 // Show the connect panel on first load unless autoconnecting
97 UI
.openConnectPanel();
102 UI
.updateVisualState();
104 document
.getElementById('noVNC_setting_host').focus();
106 var autoconnect
= WebUtil
.getConfigVar('autoconnect', false);
107 if (autoconnect
=== 'true' || autoconnect
== '1') {
114 if (typeof callback
=== "function") {
119 initSettings: function() {
120 // Stylesheet selection dropdown
121 var sheet
= WebUtil
.selectStylesheet();
122 var sheets
= WebUtil
.getStylesheets();
124 for (i
= 0; i
< sheets
.length
; i
+= 1) {
125 UI
.addOption(document
.getElementById('noVNC_setting_stylesheet'),sheets
[i
].title
, sheets
[i
].title
);
128 // Logging selection dropdown
129 var llevels
= ['error', 'warn', 'info', 'debug'];
130 for (i
= 0; i
< llevels
.length
; i
+= 1) {
131 UI
.addOption(document
.getElementById('noVNC_setting_logging'),llevels
[i
], llevels
[i
]);
134 // Settings with immediate effects
135 UI
.initSetting('logging', 'warn');
136 WebUtil
.init_logging(UI
.getSetting('logging'));
138 UI
.initSetting('stylesheet', 'default');
139 WebUtil
.selectStylesheet(null);
140 // call twice to get around webkit bug
141 WebUtil
.selectStylesheet(UI
.getSetting('stylesheet'));
143 // if port == 80 (or 443) then it won't be present and should be
145 var port
= window
.location
.port
;
147 if (window
.location
.protocol
.substring(0,5) == 'https') {
150 else if (window
.location
.protocol
.substring(0,4) == 'http') {
155 /* Populate the controls if defaults are provided in the URL */
156 UI
.initSetting('host', window
.location
.hostname
);
157 UI
.initSetting('port', port
);
158 UI
.initSetting('password', '');
159 UI
.initSetting('encrypt', (window
.location
.protocol
=== "https:"));
160 UI
.initSetting('true_color', true);
161 UI
.initSetting('cursor', !Util
.isTouchDevice
);
162 UI
.initSetting('resize', 'off');
163 UI
.initSetting('shared', true);
164 UI
.initSetting('view_only', false);
165 UI
.initSetting('path', 'websockify');
166 UI
.initSetting('repeaterID', '');
167 UI
.initSetting('token', '');
170 setupWindowEvents: function() {
171 window
.addEventListener( 'resize', function () {
172 UI
.applyResizeMode();
177 document
.getElementById("noVNC_status")
178 .addEventListener('click', UI
.hideStatus
);
181 setupFullscreen: function() {
182 // Only show the button if fullscreen is properly supported
183 // * Safari doesn't support alphanumerical input while in fullscreen
185 (document
.documentElement
.requestFullscreen
||
186 document
.documentElement
.mozRequestFullScreen
||
187 document
.documentElement
.webkitRequestFullscreen
||
188 document
.body
.msRequestFullscreen
)) {
189 document
.getElementById('noVNC_fullscreen_button')
190 .classList
.remove("noVNC_hidden");
191 UI
.addFullscreenHandlers();
195 addControlbarHandlers: function() {
196 document
.getElementById("noVNC_control_bar")
197 .addEventListener('mousemove', UI
.activateControlbar
);
198 document
.getElementById("noVNC_control_bar")
199 .addEventListener('mouseup', UI
.activateControlbar
);
200 document
.getElementById("noVNC_control_bar")
201 .addEventListener('mousedown', UI
.activateControlbar
);
202 document
.getElementById("noVNC_control_bar")
203 .addEventListener('keypress', UI
.activateControlbar
);
205 document
.getElementById("noVNC_control_bar")
206 .addEventListener('mousedown', UI
.keepControlbar
);
207 document
.getElementById("noVNC_control_bar")
208 .addEventListener('keypress', UI
.keepControlbar
);
210 document
.getElementById("noVNC_view_drag_button")
211 .addEventListener('click', UI
.toggleViewDrag
);
213 document
.getElementById("noVNC_control_bar_handle")
214 .addEventListener('mousedown', UI
.controlbarHandleMouseDown
);
215 document
.getElementById("noVNC_control_bar_handle")
216 .addEventListener('mouseup', UI
.controlbarHandleMouseUp
);
217 document
.getElementById("noVNC_control_bar_handle")
218 .addEventListener('mousemove', UI
.dragControlbarHandle
);
219 // resize events aren't available for elements
220 window
.addEventListener('resize', UI
.updateControlbarHandle
);
223 addTouchSpecificHandlers: function() {
224 document
.getElementById("noVNC_mouse_button0")
225 .addEventListener('click', function () { UI
.setMouseButton(1); });
226 document
.getElementById("noVNC_mouse_button1")
227 .addEventListener('click', function () { UI
.setMouseButton(2); });
228 document
.getElementById("noVNC_mouse_button2")
229 .addEventListener('click', function () { UI
.setMouseButton(4); });
230 document
.getElementById("noVNC_mouse_button4")
231 .addEventListener('click', function () { UI
.setMouseButton(0); });
232 document
.getElementById("noVNC_keyboard_button")
233 .addEventListener('click', UI
.toggleVirtualKeyboard
);
235 document
.getElementById("noVNC_keyboardinput")
236 .addEventListener('input', UI
.keyInput
);
237 document
.getElementById("noVNC_keyboardinput")
238 .addEventListener('blur', UI
.onblurVirtualKeyboard
);
239 document
.getElementById("noVNC_keyboardinput")
240 .addEventListener('submit', function () { return false; });
242 document
.getElementById("noVNC_control_bar")
243 .addEventListener('touchstart', UI
.activateControlbar
);
244 document
.getElementById("noVNC_control_bar")
245 .addEventListener('touchmove', UI
.activateControlbar
);
246 document
.getElementById("noVNC_control_bar")
247 .addEventListener('touchend', UI
.activateControlbar
);
248 document
.getElementById("noVNC_control_bar")
249 .addEventListener('input', UI
.activateControlbar
);
251 document
.getElementById("noVNC_control_bar")
252 .addEventListener('touchstart', UI
.keepControlbar
);
253 document
.getElementById("noVNC_control_bar")
254 .addEventListener('input', UI
.keepControlbar
);
256 document
.getElementById("noVNC_control_bar_handle")
257 .addEventListener('touchstart', UI
.controlbarHandleMouseDown
);
258 document
.getElementById("noVNC_control_bar_handle")
259 .addEventListener('touchend', UI
.controlbarHandleMouseUp
);
260 document
.getElementById("noVNC_control_bar_handle")
261 .addEventListener('touchmove', UI
.dragControlbarHandle
);
263 window
.addEventListener('load', UI
.keyboardinputReset
);
266 addExtraKeysHandlers: function() {
267 document
.getElementById("noVNC_toggle_extra_keys_button")
268 .addEventListener('click', UI
.toggleExtraKeys
);
269 document
.getElementById("noVNC_toggle_ctrl_button")
270 .addEventListener('click', UI
.toggleCtrl
);
271 document
.getElementById("noVNC_toggle_alt_button")
272 .addEventListener('click', UI
.toggleAlt
);
273 document
.getElementById("noVNC_send_tab_button")
274 .addEventListener('click', UI
.sendTab
);
275 document
.getElementById("noVNC_send_esc_button")
276 .addEventListener('click', UI
.sendEsc
);
277 document
.getElementById("noVNC_send_ctrl_alt_del_button")
278 .addEventListener('click', UI
.sendCtrlAltDel
);
281 addXvpHandlers: function() {
282 document
.getElementById("noVNC_xvp_shutdown_button")
283 .addEventListener('click', function() { UI
.rfb
.xvpShutdown(); });
284 document
.getElementById("noVNC_xvp_reboot_button")
285 .addEventListener('click', function() { UI
.rfb
.xvpReboot(); });
286 document
.getElementById("noVNC_xvp_reset_button")
287 .addEventListener('click', function() { UI
.rfb
.xvpReset(); });
288 document
.getElementById("noVNC_xvp_button")
289 .addEventListener('click', UI
.toggleXvpPanel
);
292 addConnectionControlHandlers: function() {
293 document
.getElementById("noVNC_connect_controls_button")
294 .addEventListener('click', UI
.toggleConnectPanel
);
295 document
.getElementById("noVNC_disconnect_button")
296 .addEventListener('click', UI
.disconnect
);
297 document
.getElementById("noVNC_connect_button")
298 .addEventListener('click', UI
.connect
);
300 document
.getElementById("noVNC_password_button")
301 .addEventListener('click', UI
.setPassword
);
304 addClipboardHandlers: function() {
305 document
.getElementById("noVNC_clipboard_button")
306 .addEventListener('click', UI
.toggleClipboardPanel
);
307 document
.getElementById("noVNC_clipboard_text")
308 .addEventListener('focus', UI
.displayBlur
);
309 document
.getElementById("noVNC_clipboard_text")
310 .addEventListener('blur', UI
.displayFocus
);
311 document
.getElementById("noVNC_clipboard_text")
312 .addEventListener('change', UI
.clipboardSend
);
313 document
.getElementById("noVNC_clipboard_clear_button")
314 .addEventListener('click', UI
.clipboardClear
);
317 addSettingsHandlers: function() {
318 document
.getElementById("noVNC_settings_button")
319 .addEventListener('click', UI
.toggleSettingsPanel
);
320 document
.getElementById("noVNC_settings_apply")
321 .addEventListener('click', UI
.settingsApply
);
323 document
.getElementById("noVNC_setting_resize")
324 .addEventListener('change', UI
.enableDisableViewClip
);
327 addFullscreenHandlers: function() {
328 document
.getElementById("noVNC_fullscreen_button")
329 .addEventListener('click', UI
.toggleFullscreen
);
331 window
.addEventListener('fullscreenchange', UI
.updateFullscreenButton
);
332 window
.addEventListener('mozfullscreenchange', UI
.updateFullscreenButton
);
333 window
.addEventListener('webkitfullscreenchange', UI
.updateFullscreenButton
);
334 window
.addEventListener('msfullscreenchange', UI
.updateFullscreenButton
);
337 initRFB: function() {
339 UI
.rfb
= new RFB({'target': document
.getElementById('noVNC_canvas'),
340 'onNotification': UI
.notification
,
341 'onUpdateState': UI
.updateState
,
342 'onDisconnected': UI
.disconnectFinished
,
343 'onPasswordRequired': UI
.passwordRequired
,
344 'onXvpInit': UI
.updateXvpButton
,
345 'onClipboard': UI
.clipboardReceive
,
347 'onFBUComplete': UI
.initialResize
,
348 'onFBResize': UI
.updateViewDrag
,
349 'onDesktopName': UI
.updateDesktopName
});
352 var msg
= 'Unable to create RFB client -- ' + exc
;
354 UI
.showStatus(msg
, 'error');
365 updateState: function(rfb
, state
, oldstate
) {
369 UI
.showStatus("Connecting");
373 if (rfb
&& rfb
.get_encrypt()) {
374 msg
= "Connected (encrypted) to " + UI
.desktopName
;
376 msg
= "Connected (unencrypted) to " + UI
.desktopName
;
380 case 'disconnecting':
381 UI
.showStatus("Disconnecting");
384 UI
.connected
= false;
385 UI
.showStatus("Disconnected");
388 msg
= "Invalid UI state";
390 UI
.showStatus(msg
, 'error');
394 UI
.updateVisualState();
397 // Disable/enable controls depending on connection state
398 updateVisualState: function() {
399 //Util.Debug(">> updateVisualState");
400 document
.getElementById('noVNC_setting_encrypt').disabled
= UI
.connected
;
401 document
.getElementById('noVNC_setting_true_color').disabled
= UI
.connected
;
402 if (Util
.browserSupportsCursorURIs()) {
403 document
.getElementById('noVNC_setting_cursor').disabled
= UI
.connected
;
405 UI
.updateSetting('cursor', !Util
.isTouchDevice
);
406 document
.getElementById('noVNC_setting_cursor').disabled
= true;
409 UI
.enableDisableViewClip();
410 document
.getElementById('noVNC_setting_resize').disabled
= UI
.connected
;
411 document
.getElementById('noVNC_setting_shared').disabled
= UI
.connected
;
412 document
.getElementById('noVNC_setting_view_only').disabled
= UI
.connected
;
413 document
.getElementById('noVNC_setting_path').disabled
= UI
.connected
;
414 document
.getElementById('noVNC_setting_repeaterID').disabled
= UI
.connected
;
417 document
.documentElement
.classList
.add("noVNC_connected");
419 UI
.setMouseButton(1);
421 // Hide the controlbar after 2 seconds
422 UI
.closeControlbarTimeout
= setTimeout(UI
.closeControlbar
, 2000);
424 document
.documentElement
.classList
.remove("noVNC_connected");
425 UI
.updateXvpButton(0);
429 // Hide input related buttons in view only mode
430 if (UI
.rfb
&& UI
.rfb
.get_view_only()) {
431 document
.getElementById('noVNC_keyboard_button')
432 .classList
.add('noVNC_hidden');
433 document
.getElementById('noVNC_toggle_extra_keys_button')
434 .classList
.add('noVNC_hidden');
436 document
.getElementById('noVNC_keyboard_button')
437 .classList
.remove('noVNC_hidden');
438 document
.getElementById('noVNC_toggle_extra_keys_button')
439 .classList
.remove('noVNC_hidden');
442 // State change disables viewport dragging.
443 // It is enabled (toggled) by direct click on the button
444 UI
.setViewDrag(false);
446 // State change also closes the password dialog
447 document
.getElementById('noVNC_password_dlg')
448 .classList
.remove('noVNC_open');
450 //Util.Debug("<< updateVisualState");
453 showStatus: function(text
, status_type
, time
) {
454 var statusElem
= document
.getElementById('noVNC_status');
456 clearTimeout(UI
.statusTimeout
);
458 if (typeof status_type
=== 'undefined') {
459 status_type
= 'normal';
462 statusElem
.classList
.remove("noVNC_status_normal",
464 "noVNC_status_error");
466 switch (status_type
) {
469 statusElem
.classList
.add("noVNC_status_warn");
472 statusElem
.classList
.add("noVNC_status_error");
477 statusElem
.classList
.add("noVNC_status_normal");
481 statusElem
.innerHTML
= text
;
482 statusElem
.classList
.add("noVNC_open");
484 // If no time was specified, show the status for 1.5 seconds
485 if (typeof time
=== 'undefined') {
489 // Error messages do not timeout
490 if (status_type
!== 'error') {
491 UI
.statusTimeout
= window
.setTimeout(UI
.hideStatus
, time
);
495 hideStatus: function() {
496 clearTimeout(UI
.statusTimeout
);
497 document
.getElementById('noVNC_status').classList
.remove("noVNC_open");
500 notification: function (rfb
, msg
, level
, options
) {
501 UI
.showStatus(msg
, level
);
504 activateControlbar: function(event
) {
505 clearTimeout(UI
.idleControlbarTimeout
);
506 // We manipulate the anchor instead of the actual control
507 // bar in order to avoid creating new a stacking group
508 document
.getElementById('noVNC_control_bar_anchor')
509 .classList
.remove("noVNC_idle");
510 UI
.idleControlbarTimeout
= window
.setTimeout(UI
.idleControlbar
, 2000);
513 idleControlbar: function() {
514 document
.getElementById('noVNC_control_bar_anchor')
515 .classList
.add("noVNC_idle");
518 keepControlbar: function() {
519 clearTimeout(UI
.closeControlbarTimeout
);
522 openControlbar: function() {
523 document
.getElementById('noVNC_control_bar')
524 .classList
.add("noVNC_open");
527 closeControlbar: function() {
529 document
.getElementById('noVNC_control_bar')
530 .classList
.remove("noVNC_open");
533 toggleControlbar: function() {
534 if (document
.getElementById('noVNC_control_bar')
535 .classList
.contains("noVNC_open")) {
536 UI
.closeControlbar();
542 dragControlbarHandle: function (e
) {
543 if (!UI
.controlbarGrabbed
) return;
545 var ptr
= Util
.getPointerEvent(e
);
547 if (!UI
.controlbarDrag
) {
548 // The goal is to trigger on a certain physical width, the
549 // devicePixelRatio brings us a bit closer but is not optimal.
550 var dragThreshold
= 10 * (window
.devicePixelRatio
|| 1);
551 var dragDistance
= Math
.abs(ptr
.clientY
- UI
.controlbarMouseDownClientY
);
553 if (dragDistance
< dragThreshold
) return;
555 UI
.controlbarDrag
= true;
558 var eventY
= ptr
.clientY
- UI
.controlbarMouseDownOffsetY
;
560 UI
.moveControlbarHandle(eventY
);
566 // Move the handle but don't allow any position outside the bounds
567 moveControlbarHandle: function (viewportRelativeY
) {
568 var handle
= document
.getElementById("noVNC_control_bar_handle");
569 var handleHeight
= handle
.getBoundingClientRect().height
;
570 var controlbarBounds
= document
.getElementById("noVNC_control_bar")
571 .getBoundingClientRect();
574 // These heights need to be non-zero for the below logic to work
575 if (handleHeight
=== 0 || controlbarBounds
.height
=== 0) {
579 var newY
= viewportRelativeY
;
581 // Check if the coordinates are outside the control bar
582 if (newY
< controlbarBounds
.top
+ margin
) {
583 // Force coordinates to be below the top of the control bar
584 newY
= controlbarBounds
.top
+ margin
;
586 } else if (newY
> controlbarBounds
.top
+
587 controlbarBounds
.height
- handleHeight
- margin
) {
588 // Force coordinates to be above the bottom of the control bar
589 newY
= controlbarBounds
.top
+
590 controlbarBounds
.height
- handleHeight
- margin
;
593 // Corner case: control bar too small for stable position
594 if (controlbarBounds
.height
< (handleHeight
+ margin
* 2)) {
595 newY
= controlbarBounds
.top
+
596 (controlbarBounds
.height
- handleHeight
) / 2;
599 // The transform needs coordinates that are relative to the parent
600 var parentRelativeY
= newY
- controlbarBounds
.top
;
601 handle
.style
.transform
= "translateY(" + parentRelativeY
+ "px)";
604 updateControlbarHandle: function () {
605 // Since the control bar is fixed on the viewport and not the page,
606 // the move function expects coordinates relative the the viewport.
607 var handle
= document
.getElementById("noVNC_control_bar_handle");
608 var handleBounds
= handle
.getBoundingClientRect();
609 UI
.moveControlbarHandle(handleBounds
.top
);
612 controlbarHandleMouseUp: function(e
) {
613 if ((e
.type
== "mouseup") && (e
.button
!= 0)) return;
615 // mouseup and mousedown on the same place toggles the controlbar
616 if (UI
.controlbarGrabbed
&& !UI
.controlbarDrag
) {
617 UI
.toggleControlbar();
621 UI
.controlbarGrabbed
= false;
624 controlbarHandleMouseDown: function(e
) {
625 if ((e
.type
== "mousedown") && (e
.button
!= 0)) return;
627 var ptr
= Util
.getPointerEvent(e
);
629 var handle
= document
.getElementById("noVNC_control_bar_handle");
630 var bounds
= handle
.getBoundingClientRect();
632 WebUtil
.setCapture(handle
);
633 UI
.controlbarGrabbed
= true;
634 UI
.controlbarDrag
= false;
636 UI
.controlbarMouseDownClientY
= ptr
.clientY
;
637 UI
.controlbarMouseDownOffsetY
= ptr
.clientY
- bounds
.top
;
648 // Initial page load read/initialization of settings
649 initSetting: function(name
, defVal
) {
650 // Check Query string followed by cookie
651 var val
= WebUtil
.getConfigVar(name
);
653 val
= WebUtil
.readSetting(name
, defVal
);
655 UI
.updateSetting(name
, val
);
659 // Update cookie and form control setting. If value is not set, then
660 // updates from control to current cookie setting.
661 updateSetting: function(name
, value
) {
663 // Save the cookie for this session
664 if (typeof value
!== 'undefined') {
665 WebUtil
.writeSetting(name
, value
);
668 // Update the settings control
669 value
= UI
.getSetting(name
);
671 var ctrl
= document
.getElementById('noVNC_setting_' + name
);
672 if (ctrl
.type
=== 'checkbox') {
673 ctrl
.checked
= value
;
675 } else if (typeof ctrl
.options
!== 'undefined') {
676 for (var i
= 0; i
< ctrl
.options
.length
; i
+= 1) {
677 if (ctrl
.options
[i
].value
=== value
) {
678 ctrl
.selectedIndex
= i
;
683 /*Weird IE9 error leads to 'null' appearring
684 in textboxes instead of ''.*/
685 if (value
=== null) {
692 // Save control setting to cookie
693 saveSetting: function(name
) {
694 var val
, ctrl
= document
.getElementById('noVNC_setting_' + name
);
695 if (ctrl
.type
=== 'checkbox') {
697 } else if (typeof ctrl
.options
!== 'undefined') {
698 val
= ctrl
.options
[ctrl
.selectedIndex
].value
;
702 WebUtil
.writeSetting(name
, val
);
703 //Util.Debug("Setting saved '" + name + "=" + val + "'");
707 // Force a setting to be a certain value
708 forceSetting: function(name
, val
) {
709 UI
.updateSetting(name
, val
);
713 // Read form control compatible setting from cookie
714 getSetting: function(name
) {
715 var ctrl
= document
.getElementById('noVNC_setting_' + name
);
716 var val
= WebUtil
.readSetting(name
);
717 if (typeof val
!== 'undefined' && val
!== null && ctrl
.type
=== 'checkbox') {
718 if (val
.toString().toLowerCase() in {'0':1, 'no':1, 'false':1}) {
727 // Save/apply settings when 'Apply' button is pressed
728 settingsApply: function() {
729 //Util.Debug(">> settingsApply");
730 UI
.saveSetting('encrypt');
731 UI
.saveSetting('true_color');
732 if (Util
.browserSupportsCursorURIs()) {
733 UI
.saveSetting('cursor');
736 UI
.saveSetting('resize');
738 if (UI
.getSetting('resize') === 'downscale' || UI
.getSetting('resize') === 'scale') {
739 UI
.forceSetting('clip', false);
742 UI
.saveSetting('clip');
743 UI
.saveSetting('shared');
744 UI
.saveSetting('view_only');
745 UI
.saveSetting('path');
746 UI
.saveSetting('repeaterID');
747 UI
.saveSetting('stylesheet');
748 UI
.saveSetting('logging');
750 // Settings with immediate (non-connected related) effect
751 WebUtil
.selectStylesheet(UI
.getSetting('stylesheet'));
752 WebUtil
.init_logging(UI
.getSetting('logging'));
755 //Util.Debug("<< settingsApply");
764 closeAllPanels: function() {
765 UI
.closeSettingsPanel();
767 UI
.closeClipboardPanel();
768 UI
.closeConnectPanel();
778 openSettingsPanel: function() {
782 UI
.updateSetting('encrypt');
783 UI
.updateSetting('true_color');
784 if (Util
.browserSupportsCursorURIs()) {
785 UI
.updateSetting('cursor');
787 UI
.updateSetting('cursor', !Util
.isTouchDevice
);
788 document
.getElementById('noVNC_setting_cursor').disabled
= true;
790 UI
.updateSetting('clip');
791 UI
.updateSetting('resize');
792 UI
.updateSetting('shared');
793 UI
.updateSetting('view_only');
794 UI
.updateSetting('path');
795 UI
.updateSetting('repeaterID');
796 UI
.updateSetting('stylesheet');
797 UI
.updateSetting('logging');
799 document
.getElementById('noVNC_settings')
800 .classList
.add("noVNC_open");
801 document
.getElementById('noVNC_settings_button')
802 .classList
.add("noVNC_selected");
805 closeSettingsPanel: function() {
806 document
.getElementById('noVNC_settings')
807 .classList
.remove("noVNC_open");
808 document
.getElementById('noVNC_settings_button')
809 .classList
.remove("noVNC_selected");
812 // Toggle the settings menu:
813 // On open, settings are refreshed from saved cookies.
814 // On close, settings are applied
815 toggleSettingsPanel: function() {
816 if (document
.getElementById('noVNC_settings')
817 .classList
.contains("noVNC_open")) {
819 UI
.closeSettingsPanel();
821 UI
.openSettingsPanel();
831 openXvpPanel: function() {
835 document
.getElementById('noVNC_xvp')
836 .classList
.add("noVNC_open");
837 document
.getElementById('noVNC_xvp_button')
838 .classList
.add("noVNC_selected");
841 closeXvpPanel: function() {
842 document
.getElementById('noVNC_xvp')
843 .classList
.remove("noVNC_open");
844 document
.getElementById('noVNC_xvp_button')
845 .classList
.remove("noVNC_selected");
848 toggleXvpPanel: function() {
849 if (document
.getElementById('noVNC_xvp')
850 .classList
.contains("noVNC_open")) {
857 // Disable/enable XVP button
858 updateXvpButton: function(ver
) {
859 if (ver
>= 1 && !UI
.rfb
.get_view_only()) {
860 document
.getElementById('noVNC_xvp_button')
861 .classList
.remove("noVNC_hidden");
863 document
.getElementById('noVNC_xvp_button')
864 .classList
.add("noVNC_hidden");
865 // Close XVP panel if open
876 openClipboardPanel: function() {
880 document
.getElementById('noVNC_clipboard')
881 .classList
.add("noVNC_open");
882 document
.getElementById('noVNC_clipboard_button')
883 .classList
.add("noVNC_selected");
886 closeClipboardPanel: function() {
887 document
.getElementById('noVNC_clipboard')
888 .classList
.remove("noVNC_open");
889 document
.getElementById('noVNC_clipboard_button')
890 .classList
.remove("noVNC_selected");
893 toggleClipboardPanel: function() {
894 if (document
.getElementById('noVNC_clipboard')
895 .classList
.contains("noVNC_open")) {
896 UI
.closeClipboardPanel();
898 UI
.openClipboardPanel();
902 clipboardReceive: function(rfb
, text
) {
903 Util
.Debug(">> UI.clipboardReceive: " + text
.substr(0,40) + "...");
904 document
.getElementById('noVNC_clipboard_text').value
= text
;
905 Util
.Debug("<< UI.clipboardReceive");
908 clipboardClear: function() {
909 document
.getElementById('noVNC_clipboard_text').value
= "";
910 UI
.rfb
.clipboardPasteFrom("");
913 clipboardSend: function() {
914 var text
= document
.getElementById('noVNC_clipboard_text').value
;
915 Util
.Debug(">> UI.clipboardSend: " + text
.substr(0,40) + "...");
916 UI
.rfb
.clipboardPasteFrom(text
);
917 Util
.Debug("<< UI.clipboardSend");
926 openConnectPanel: function() {
930 document
.getElementById('noVNC_connect_controls')
931 .classList
.add("noVNC_open");
932 document
.getElementById('noVNC_connect_controls_button')
933 .classList
.add("noVNC_selected");
935 document
.getElementById('noVNC_setting_host').focus();
938 closeConnectPanel: function() {
939 document
.getElementById('noVNC_connect_controls')
940 .classList
.remove("noVNC_open");
941 document
.getElementById('noVNC_connect_controls_button')
942 .classList
.remove("noVNC_selected");
944 UI
.saveSetting('host');
945 UI
.saveSetting('port');
946 UI
.saveSetting('token');
947 //UI.saveSetting('password');
950 toggleConnectPanel: function() {
951 if (document
.getElementById('noVNC_connect_controls')
952 .classList
.contains("noVNC_open")) {
953 UI
.closeConnectPanel();
955 UI
.openConnectPanel();
959 connect: function() {
960 var host
= document
.getElementById('noVNC_setting_host').value
;
961 var port
= document
.getElementById('noVNC_setting_port').value
;
962 var password
= document
.getElementById('noVNC_setting_password').value
;
963 var token
= document
.getElementById('noVNC_setting_token').value
;
964 var path
= document
.getElementById('noVNC_setting_path').value
;
966 //if token is in path then ignore the new token variable
968 path
= WebUtil
.injectParamIfMissing(path
, "token", token
);
971 if ((!host
) || (!port
)) {
972 var msg
= "Must set host and port";
974 UI
.showStatus(msg
, 'error');
978 if (!UI
.initRFB()) return;
982 UI
.rfb
.set_encrypt(UI
.getSetting('encrypt'));
983 UI
.rfb
.set_true_color(UI
.getSetting('true_color'));
984 UI
.rfb
.set_local_cursor(UI
.getSetting('cursor'));
985 UI
.rfb
.set_shared(UI
.getSetting('shared'));
986 UI
.rfb
.set_view_only(UI
.getSetting('view_only'));
987 UI
.rfb
.set_repeaterID(UI
.getSetting('repeaterID'));
989 UI
.rfb
.connect(host
, port
, password
, path
);
992 disconnect: function() {
996 // Restore the callback used for initial resize
997 UI
.rfb
.set_onFBUComplete(UI
.initialResize
);
999 // Don't display the connection settings until we're actually disconnected
1002 disconnectFinished: function (rfb
, reason
) {
1003 if (typeof reason
!== 'undefined') {
1004 UI
.showStatus(reason
, 'error');
1006 UI
.openConnectPanel();
1015 passwordRequired: function(rfb
, msg
) {
1017 document
.getElementById('noVNC_password_dlg')
1018 .classList
.add('noVNC_open');
1020 setTimeout(function () {
1021 document
.getElementById('noVNC_password_input').focus();
1024 if (typeof msg
=== 'undefined') {
1025 msg
= "Password is required";
1028 UI
.showStatus(msg
, "warning");
1031 setPassword: function() {
1032 UI
.rfb
.sendPassword(document
.getElementById('noVNC_password_input').value
);
1033 document
.getElementById('noVNC_password_dlg')
1034 .classList
.remove('noVNC_open');
1044 toggleFullscreen: function() {
1045 if (document
.fullscreenElement
|| // alternative standard method
1046 document
.mozFullScreenElement
|| // currently working methods
1047 document
.webkitFullscreenElement
||
1048 document
.msFullscreenElement
) {
1049 if (document
.exitFullscreen
) {
1050 document
.exitFullscreen();
1051 } else if (document
.mozCancelFullScreen
) {
1052 document
.mozCancelFullScreen();
1053 } else if (document
.webkitExitFullscreen
) {
1054 document
.webkitExitFullscreen();
1055 } else if (document
.msExitFullscreen
) {
1056 document
.msExitFullscreen();
1059 if (document
.documentElement
.requestFullscreen
) {
1060 document
.documentElement
.requestFullscreen();
1061 } else if (document
.documentElement
.mozRequestFullScreen
) {
1062 document
.documentElement
.mozRequestFullScreen();
1063 } else if (document
.documentElement
.webkitRequestFullscreen
) {
1064 document
.documentElement
.webkitRequestFullscreen(Element
.ALLOW_KEYBOARD_INPUT
);
1065 } else if (document
.body
.msRequestFullscreen
) {
1066 document
.body
.msRequestFullscreen();
1069 UI
.enableDisableViewClip();
1070 UI
.updateFullscreenButton();
1073 updateFullscreenButton: function() {
1074 if (document
.fullscreenElement
|| // alternative standard method
1075 document
.mozFullScreenElement
|| // currently working methods
1076 document
.webkitFullscreenElement
||
1077 document
.msFullscreenElement
) {
1078 document
.getElementById('noVNC_fullscreen_button')
1079 .classList
.add("noVNC_selected");
1081 document
.getElementById('noVNC_fullscreen_button')
1082 .classList
.remove("noVNC_selected");
1092 // Apply remote resizing or local scaling
1093 applyResizeMode: function() {
1094 if (!UI
.rfb
) return;
1096 var screen
= UI
.screenSize();
1098 if (screen
&& UI
.connected
&& UI
.rfb
.get_display()) {
1100 var display
= UI
.rfb
.get_display();
1101 var resizeMode
= UI
.getSetting('resize');
1103 if (resizeMode
=== 'remote') {
1105 // Request changing the resolution of the remote display to
1106 // the size of the local browser viewport.
1108 // In order to not send multiple requests before the browser-resize
1109 // is finished we wait 0.5 seconds before sending the request.
1110 clearTimeout(UI
.resizeTimeout
);
1111 UI
.resizeTimeout
= setTimeout(function(){
1113 // Limit the viewport to the size of the browser window
1114 display
.set_maxWidth(screen
.w
);
1115 display
.set_maxHeight(screen
.h
);
1117 // Request a remote size covering the viewport
1118 if (UI
.rfb
.requestDesktopSize(screen
.w
, screen
.h
)) {
1119 Util
.Debug('Requested new desktop size: ' +
1120 screen
.w
+ 'x' + screen
.h
);
1124 } else if (resizeMode
=== 'scale' || resizeMode
=== 'downscale') {
1125 var downscaleOnly
= resizeMode
=== 'downscale';
1126 var scaleRatio
= display
.autoscale(screen
.w
, screen
.h
, downscaleOnly
);
1128 if (!UI
.rfb
.get_view_only()) {
1129 UI
.rfb
.get_mouse().set_scale(scaleRatio
);
1130 Util
.Debug('Scaling by ' + UI
.rfb
.get_mouse().get_scale());
1136 // The screen is always the same size as the available viewport
1137 // in the browser window minus the height of the control bar
1138 screenSize: function() {
1139 var screen
= document
.getElementById('noVNC_screen');
1141 // Hide the scrollbars until the size is calculated
1142 screen
.style
.overflow
= "hidden";
1144 var pos
= Util
.getPosition(screen
);
1148 screen
.style
.overflow
= "visible";
1150 if (isNaN(w
) || isNaN(h
)) {
1153 return {w
: w
, h
: h
};
1157 // Normally we only apply the current resize mode after a window resize
1158 // event. This means that when a new connection is opened, there is no
1159 // resize mode active.
1160 // We have to wait until the first FBU because this is where the client
1161 // will find the supported encodings of the server. Some calls later in
1162 // the chain is dependant on knowing the server-capabilities.
1163 initialResize: function(rfb
, fbu
) {
1164 UI
.applyResizeMode();
1165 // After doing this once, we remove the callback.
1166 UI
.rfb
.set_onFBUComplete(function() { });
1175 // Set and configure viewport clipping
1176 setViewClip: function(clip
) {
1177 UI
.updateSetting('clip', clip
);
1178 UI
.updateViewClip();
1181 // Update parameters that depend on the clip setting
1182 updateViewClip: function() {
1183 if (!UI
.rfb
) return;
1185 var display
= UI
.rfb
.get_display();
1186 var cur_clip
= display
.get_viewport();
1187 var new_clip
= UI
.getSetting('clip');
1189 if (cur_clip
!== new_clip
) {
1190 display
.set_viewport(new_clip
);
1193 var size
= UI
.screenSize();
1195 if (new_clip
&& size
) {
1196 // When clipping is enabled, the screen is limited to
1197 // the size of the browser window.
1198 display
.set_maxWidth(size
.w
);
1199 display
.set_maxHeight(size
.h
);
1201 var screen
= document
.getElementById('noVNC_screen');
1202 var canvas
= document
.getElementById('noVNC_canvas');
1204 // Hide potential scrollbars that can skew the position
1205 screen
.style
.overflow
= "hidden";
1207 // The x position marks the left margin of the canvas,
1208 // remove the margin from both sides to keep it centered.
1209 var new_w
= size
.w
- (2 * Util
.getPosition(canvas
).x
);
1211 screen
.style
.overflow
= "visible";
1213 display
.viewportChangeSize(new_w
, size
.h
);
1215 // Disable max dimensions
1216 display
.set_maxWidth(0);
1217 display
.set_maxHeight(0);
1218 display
.viewportChangeSize();
1222 // Handle special cases where clipping is forced on/off or locked
1223 enableDisableViewClip: function() {
1224 var resizeSetting
= document
.getElementById('noVNC_setting_resize');
1227 // Safari auto-hides the scrollbars which makes them
1228 // impossible to use in most cases
1229 UI
.setViewClip(true);
1230 document
.getElementById('noVNC_setting_clip').disabled
= true;
1231 } else if (resizeSetting
.value
=== 'downscale' || resizeSetting
.value
=== 'scale') {
1232 // Disable clipping if we are scaling
1233 UI
.setViewClip(false);
1234 document
.getElementById('noVNC_setting_clip').disabled
= true;
1235 } else if (document
.msFullscreenElement
) {
1236 // The browser is IE and we are in fullscreen mode.
1237 // - We need to force clipping while in fullscreen since
1238 // scrollbars doesn't work.
1239 var msg
= "Forcing clipping mode since scrollbars aren't" +
1240 "supported by IE in fullscreen";
1243 UI
.rememberedClipSetting
= UI
.getSetting('clip');
1244 UI
.setViewClip(true);
1245 document
.getElementById('noVNC_setting_clip').disabled
= true;
1246 } else if (document
.body
.msRequestFullscreen
&&
1247 UI
.rememberedClipSetting
!== null) {
1248 // Restore view clip to what it was before fullscreen on IE
1249 UI
.setViewClip(UI
.rememberedClipSetting
);
1250 document
.getElementById('noVNC_setting_clip').disabled
=
1251 UI
.connected
|| Util
.isTouchDevice
;
1253 document
.getElementById('noVNC_setting_clip').disabled
=
1254 UI
.connected
|| Util
.isTouchDevice
;
1255 if (Util
.isTouchDevice
) {
1256 UI
.setViewClip(true);
1267 toggleViewDrag: function() {
1268 if (!UI
.rfb
) return;
1270 var drag
= UI
.rfb
.get_viewportDrag();
1271 UI
.setViewDrag(!drag
);
1274 // Set the view drag mode which moves the viewport on mouse drags
1275 setViewDrag: function(drag
) {
1276 if (!UI
.rfb
) return;
1278 UI
.rfb
.set_viewportDrag(drag
);
1280 UI
.updateViewDrag();
1283 updateViewDrag: function() {
1284 var clipping
= false;
1286 if (!UI
.connected
) return;
1288 // Check if viewport drag is possible. It is only possible
1289 // if the remote display is clipping the client display.
1290 if (UI
.rfb
.get_display().get_viewport() &&
1291 UI
.rfb
.get_display().clippingDisplay()) {
1295 var viewDragButton
= document
.getElementById('noVNC_view_drag_button');
1298 UI
.rfb
.get_viewportDrag()) {
1299 // The size of the remote display is the same or smaller
1300 // than the client display. Make sure viewport drag isn't
1301 // active when it can't be used.
1302 UI
.rfb
.set_viewportDrag(false);
1305 if (UI
.rfb
.get_viewportDrag()) {
1306 viewDragButton
.classList
.add("noVNC_selected");
1308 viewDragButton
.classList
.remove("noVNC_selected");
1311 // Different behaviour for touch vs non-touch
1312 // The button is disabled instead of hidden on touch devices
1313 if (Util
.isTouchDevice
) {
1314 viewDragButton
.classList
.remove("noVNC_hidden");
1317 viewDragButton
.disabled
= false;
1319 viewDragButton
.disabled
= true;
1322 viewDragButton
.disabled
= false;
1325 viewDragButton
.classList
.remove("noVNC_hidden");
1327 viewDragButton
.classList
.add("noVNC_hidden");
1338 showVirtualKeyboard: function() {
1339 if (!Util
.isTouchDevice
) return;
1341 var input
= document
.getElementById('noVNC_keyboardinput');
1343 if (document
.activeElement
== input
) return;
1345 UI
.keyboardVisible
= true;
1346 document
.getElementById('noVNC_keyboard_button')
1347 .classList
.add("noVNC_selected");
1351 var l
= input
.value
.length
;
1352 // Move the caret to the end
1353 input
.setSelectionRange(l
, l
);
1354 } catch (err
) {} // setSelectionRange is undefined in Google Chrome
1357 hideVirtualKeyboard: function() {
1358 if (!Util
.isTouchDevice
) return;
1360 var input
= document
.getElementById('noVNC_keyboardinput');
1362 if (document
.activeElement
!= input
) return;
1367 toggleVirtualKeyboard: function () {
1368 if (UI
.keyboardVisible
) {
1369 UI
.hideVirtualKeyboard();
1371 UI
.showVirtualKeyboard();
1375 onblurVirtualKeyboard: function() {
1376 //Weird bug in iOS if you change keyboardVisible
1377 //here it does not actually occur so next time
1378 //you click keyboard icon it doesnt work.
1379 UI
.hideKeyboardTimeout
= setTimeout(function() {
1380 UI
.keyboardVisible
= false;
1381 document
.getElementById('noVNC_keyboard_button')
1382 .classList
.remove("noVNC_selected");
1386 keepKeyboard: function() {
1387 clearTimeout(UI
.hideKeyboardTimeout
);
1388 if(UI
.keyboardVisible
=== true) {
1389 UI
.showVirtualKeyboard();
1390 } else if(UI
.keyboardVisible
=== false) {
1391 UI
.hideVirtualKeyboard();
1395 keyboardinputReset: function() {
1396 var kbi
= document
.getElementById('noVNC_keyboardinput');
1397 kbi
.value
= new Array(UI
.defaultKeyboardinputLen
).join("_");
1398 UI
.lastKeyboardinput
= kbi
.value
;
1401 // When normal keyboard events are left uncought, use the input events from
1402 // the keyboardinput element instead and generate the corresponding key events.
1403 // This code is required since some browsers on Android are inconsistent in
1404 // sending keyCodes in the normal keyboard events when using on screen keyboards.
1405 keyInput: function(event
) {
1407 if (!UI
.rfb
) return;
1409 var newValue
= event
.target
.value
;
1411 if (!UI
.lastKeyboardinput
) {
1412 UI
.keyboardinputReset();
1414 var oldValue
= UI
.lastKeyboardinput
;
1418 // Try to check caret position since whitespace at the end
1419 // will not be considered by value.length in some browsers
1420 newLen
= Math
.max(event
.target
.selectionStart
, newValue
.length
);
1422 // selectionStart is undefined in Google Chrome
1423 newLen
= newValue
.length
;
1425 var oldLen
= oldValue
.length
;
1428 var inputs
= newLen
- oldLen
;
1430 backspaces
= -inputs
;
1435 // Compare the old string with the new to account for
1436 // text-corrections or other input that modify existing text
1438 for (i
= 0; i
< Math
.min(oldLen
, newLen
); i
++) {
1439 if (newValue
.charAt(i
) != oldValue
.charAt(i
)) {
1440 inputs
= newLen
- i
;
1441 backspaces
= oldLen
- i
;
1446 // Send the key events
1447 for (i
= 0; i
< backspaces
; i
++) {
1448 UI
.rfb
.sendKey(KeyTable
.XK_BackSpace
);
1450 for (i
= newLen
- inputs
; i
< newLen
; i
++) {
1451 UI
.rfb
.sendKey(newValue
.charCodeAt(i
));
1454 // Control the text content length in the keyboardinput element
1455 if (newLen
> 2 * UI
.defaultKeyboardinputLen
) {
1456 UI
.keyboardinputReset();
1457 } else if (newLen
< 1) {
1458 // There always have to be some text in the keyboardinput
1459 // element with which backspace can interact.
1460 UI
.keyboardinputReset();
1461 // This sometimes causes the keyboard to disappear for a second
1462 // but it is required for the android keyboard to recognize that
1463 // text has been added to the field
1464 event
.target
.blur();
1465 // This has to be ran outside of the input handler in order to work
1466 setTimeout(UI
.keepKeyboard
, 0);
1468 UI
.lastKeyboardinput
= newValue
;
1478 openExtraKeys: function() {
1479 UI
.closeAllPanels();
1480 UI
.openControlbar();
1482 document
.getElementById('noVNC_modifiers')
1483 .classList
.add("noVNC_open");
1484 document
.getElementById('noVNC_toggle_extra_keys_button')
1485 .classList
.add("noVNC_selected");
1488 closeExtraKeys: function() {
1489 document
.getElementById('noVNC_modifiers')
1490 .classList
.remove("noVNC_open");
1491 document
.getElementById('noVNC_toggle_extra_keys_button')
1492 .classList
.remove("noVNC_selected");
1495 toggleExtraKeys: function() {
1497 if(document
.getElementById('noVNC_modifiers')
1498 .classList
.contains("noVNC_open")) {
1499 UI
.closeExtraKeys();
1505 sendEsc: function() {
1507 UI
.rfb
.sendKey(KeyTable
.XK_Escape
);
1510 sendTab: function() {
1512 UI
.rfb
.sendKey(KeyTable
.XK_Tab
);
1515 toggleCtrl: function() {
1517 var btn
= document
.getElementById('noVNC_toggle_ctrl_button');
1518 if (btn
.classList
.contains("noVNC_selected")) {
1519 UI
.rfb
.sendKey(KeyTable
.XK_Control_L
, false);
1520 btn
.classList
.remove("noVNC_selected");
1522 UI
.rfb
.sendKey(KeyTable
.XK_Control_L
, true);
1523 btn
.classList
.add("noVNC_selected");
1527 toggleAlt: function() {
1529 var btn
= document
.getElementById('noVNC_toggle_alt_button');
1530 if (btn
.classList
.contains("noVNC_selected")) {
1531 UI
.rfb
.sendKey(KeyTable
.XK_Alt_L
, false);
1532 btn
.classList
.remove("noVNC_selected");
1534 UI
.rfb
.sendKey(KeyTable
.XK_Alt_L
, true);
1535 btn
.classList
.add("noVNC_selected");
1539 sendCtrlAltDel: function() {
1541 UI
.rfb
.sendCtrlAltDel();
1550 setMouseButton: function(num
) {
1551 var view_only
= UI
.rfb
.get_view_only();
1552 if (UI
.rfb
&& !view_only
) {
1553 UI
.rfb
.get_mouse().set_touchButton(num
);
1556 var blist
= [0, 1,2,4];
1557 for (var b
= 0; b
< blist
.length
; b
++) {
1558 var button
= document
.getElementById('noVNC_mouse_button' +
1560 if (blist
[b
] === num
&& !view_only
) {
1561 button
.classList
.remove("noVNC_hidden");
1563 button
.classList
.add("noVNC_hidden");
1568 displayBlur: function() {
1569 if (UI
.rfb
&& !UI
.rfb
.get_view_only()) {
1570 UI
.rfb
.get_keyboard().set_focused(false);
1571 UI
.rfb
.get_mouse().set_focused(false);
1575 displayFocus: function() {
1576 if (UI
.rfb
&& !UI
.rfb
.get_view_only()) {
1577 UI
.rfb
.get_keyboard().set_focused(true);
1578 UI
.rfb
.get_mouse().set_focused(true);
1582 updateDesktopName: function(rfb
, name
) {
1583 UI
.desktopName
= name
;
1584 // Display the desktop name in the document title
1585 document
.title
= name
+ " - noVNC";
1588 bell: function(rfb
) {
1589 if (WebUtil
.getConfigVar('bell', 'on') === 'on') {
1590 document
.getElementById('noVNC_bell').play();
1594 //Helper to add options to dropdown.
1595 addOption: function(selectbox
, text
, value
) {
1596 var optn
= document
.createElement("OPTION");
1599 selectbox
.options
.add(optn
);
1608 /* [module] UI.load(); */
1611 /* [module] export default UI; */