1/*
2 * noVNC: HTML5 VNC client
3 * Copyright (C) 2012 Joel Martin
4 * Copyright (C) 2013 Samuel Mannehed for Cendio AB
5 * Licensed under MPL 2.0 (see LICENSE.txt)
6 *
7 * See README.md for usage and integration instructions.
8 */
9
10/* jslint white: false, browser: true */
11/* global window, $D, Util, WebUtil, RFB, Display */
12
13var UI;
14
15(function () {
16    "use strict";
17
18    // Load supporting scripts
19    window.onscriptsload = function () { UI.load(); };
20    window.onload = function () { UI.keyboardinputReset(); };
21    Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js",
22                       "keysymdef.js", "keyboard.js", "input.js", "display.js",
23                       "jsunzip.js", "rfb.js", "keysym.js"]);
24
25    var UI = {
26
27        rfb_state : 'loaded',
28        settingsOpen : false,
29        connSettingsOpen : false,
30        popupStatusOpen : false,
31        clipboardOpen: false,
32        keyboardVisible: false,
33        hideKeyboardTimeout: null,
34        lastKeyboardinput: null,
35        defaultKeyboardinputLen: 100,
36        extraKeysVisible: false,
37        ctrlOn: false,
38        altOn: false,
39        isTouchDevice: false,
40
41        // Setup rfb object, load settings from browser storage, then call
42        // UI.init to setup the UI/menus
43        load: function (callback) {
44            WebUtil.initSettings(UI.start, callback);
45        },
46
47        // Render default UI and initialize settings menu
48        start: function(callback) {
49            UI.isTouchDevice = 'ontouchstart' in document.documentElement;
50
51            // Stylesheet selection dropdown
52            var sheet = WebUtil.selectStylesheet();
53            var sheets = WebUtil.getStylesheets();
54            var i;
55            for (i = 0; i < sheets.length; i += 1) {
56                UI.addOption($D('noVNC_stylesheet'),sheets[i].title, sheets[i].title);
57            }
58
59            // Logging selection dropdown
60            var llevels = ['error', 'warn', 'info', 'debug'];
61            for (i = 0; i < llevels.length; i += 1) {
62                UI.addOption($D('noVNC_logging'),llevels[i], llevels[i]);
63            }
64
65            // Settings with immediate effects
66            UI.initSetting('logging', 'warn');
67            WebUtil.init_logging(UI.getSetting('logging'));
68
69            UI.initSetting('stylesheet', 'default');
70            WebUtil.selectStylesheet(null);
71            // call twice to get around webkit bug
72            WebUtil.selectStylesheet(UI.getSetting('stylesheet'));
73
74            // if port == 80 (or 443) then it won't be present and should be
75            // set manually
76            var port = window.location.port;
77            if (!port) {
78                if (window.location.protocol.substring(0,5) == 'https') {
79                    port = 443;
80                }
81                else if (window.location.protocol.substring(0,4) == 'http') {
82                    port = 80;
83                }
84            }
85
86            /* Populate the controls if defaults are provided in the URL */
87            UI.initSetting('host', window.location.hostname);
88            UI.initSetting('port', port);
89            UI.initSetting('password', '');
90            UI.initSetting('encrypt', (window.location.protocol === "https:"));
91            UI.initSetting('true_color', true);
92            UI.initSetting('cursor', !UI.isTouchDevice);
93            UI.initSetting('shared', true);
94            UI.initSetting('view_only', false);
95            UI.initSetting('path', 'websockify');
96            UI.initSetting('repeaterID', '');
97
98            UI.rfb = new RFB({'target': $D('noVNC_canvas'),
99                              'onUpdateState': UI.updateState,
100                              'onXvpInit': UI.updateXvpVisualState,
101                              'onClipboard': UI.clipReceive,
102                              'onDesktopName': UI.updateDocumentTitle});
103
104            var autoconnect = WebUtil.getQueryVar('autoconnect', false);
105            if (autoconnect === 'true' || autoconnect == '1') {
106                autoconnect = true;
107                UI.connect();
108            } else {
109                autoconnect = false;
110            }
111
112            UI.updateVisualState();
113
114            // Show mouse selector buttons on touch screen devices
115            if (UI.isTouchDevice) {
116                // Show mobile buttons
117                $D('noVNC_mobile_buttons').style.display = "inline";
118                UI.setMouseButton();
119                // Remove the address bar
120                setTimeout(function() { window.scrollTo(0, 1); }, 100);
121                UI.forceSetting('clip', true);
122                $D('noVNC_clip').disabled = true;
123            } else {
124                UI.initSetting('clip', false);
125            }
126
127            //iOS Safari does not support CSS position:fixed.
128            //This detects iOS devices and enables javascript workaround.
129            if ((navigator.userAgent.match(/iPhone/i)) ||
130                (navigator.userAgent.match(/iPod/i)) ||
131                (navigator.userAgent.match(/iPad/i))) {
132                //UI.setOnscroll();
133                //UI.setResize();
134            }
135            UI.setBarPosition();
136
137            $D('noVNC_host').focus();
138
139            UI.setViewClip();
140            Util.addEvent(window, 'resize', UI.setViewClip);
141
142            Util.addEvent(window, 'beforeunload', function () {
143                if (UI.rfb_state === 'normal') {
144                    return "You are currently connected.";
145                }
146            } );
147
148            // Show description by default when hosted at for kanaka.github.com
149            if (location.host === "kanaka.github.io") {
150                // Open the description dialog
151                $D('noVNC_description').style.display = "block";
152            } else {
153                // Show the connect panel on first load unless autoconnecting
154                if (autoconnect === UI.connSettingsOpen) {
155                    UI.toggleConnectPanel();
156                }
157            }
158
159            // Add mouse event click/focus/blur event handlers to the UI
160            UI.addMouseHandlers();
161
162            if (typeof callback === "function") {
163                callback(UI.rfb);
164            }
165        },
166
167        addMouseHandlers: function() {
168            // Setup interface handlers that can't be inline
169            $D("noVNC_view_drag_button").onclick = UI.setViewDrag;
170            $D("noVNC_mouse_button0").onclick = function () { UI.setMouseButton(1); };
171            $D("noVNC_mouse_button1").onclick = function () { UI.setMouseButton(2); };
172            $D("noVNC_mouse_button2").onclick = function () { UI.setMouseButton(4); };
173            $D("noVNC_mouse_button4").onclick = function () { UI.setMouseButton(0); };
174            $D("showKeyboard").onclick = UI.showKeyboard;
175
176            $D("keyboardinput").oninput = UI.keyInput;
177            $D("keyboardinput").onblur = UI.keyInputBlur;
178
179            $D("showExtraKeysButton").onclick = UI.showExtraKeys;
180            $D("toggleCtrlButton").onclick = UI.toggleCtrl;
181            $D("toggleAltButton").onclick = UI.toggleAlt;
182            $D("sendTabButton").onclick = UI.sendTab;
183            $D("sendEscButton").onclick = UI.sendEsc;
184
185            $D("sendCtrlAltDelButton").onclick = UI.sendCtrlAltDel;
186            $D("xvpShutdownButton").onclick = UI.xvpShutdown;
187            $D("xvpRebootButton").onclick = UI.xvpReboot;
188            $D("xvpResetButton").onclick = UI.xvpReset;
189            $D("noVNC_status").onclick = UI.togglePopupStatusPanel;
190            $D("noVNC_popup_status_panel").onclick = UI.togglePopupStatusPanel;
191            $D("xvpButton").onclick = UI.toggleXvpPanel;
192            $D("clipboardButton").onclick = UI.toggleClipboardPanel;
193            $D("settingsButton").onclick = UI.toggleSettingsPanel;
194            $D("connectButton").onclick = UI.toggleConnectPanel;
195            $D("disconnectButton").onclick = UI.disconnect;
196            $D("descriptionButton").onclick = UI.toggleConnectPanel;
197
198            $D("noVNC_clipboard_text").onfocus = UI.displayBlur;
199            $D("noVNC_clipboard_text").onblur = UI.displayFocus;
200            $D("noVNC_clipboard_text").onchange = UI.clipSend;
201            $D("noVNC_clipboard_clear_button").onclick = UI.clipClear;
202
203            $D("noVNC_settings_menu").onmouseover = UI.displayBlur;
204            $D("noVNC_settings_menu").onmouseover = UI.displayFocus;
205            $D("noVNC_apply").onclick = UI.settingsApply;
206
207            $D("noVNC_connect_button").onclick = UI.connect;
208        },
209
210        // Read form control compatible setting from cookie
211        getSetting: function(name) {
212            var ctrl = $D('noVNC_' + name);
213            var val = WebUtil.readSetting(name);
214            if (val !== null && ctrl.type === 'checkbox') {
215                if (val.toString().toLowerCase() in {'0':1, 'no':1, 'false':1}) {
216                    val = false;
217                } else {
218                    val = true;
219                }
220            }
221            return val;
222        },
223
224        // Update cookie and form control setting. If value is not set, then
225        // updates from control to current cookie setting.
226        updateSetting: function(name, value) {
227
228            // Save the cookie for this session
229            if (typeof value !== 'undefined') {
230                WebUtil.writeSetting(name, value);
231            }
232
233            // Update the settings control
234            value = UI.getSetting(name);
235
236            var ctrl = $D('noVNC_' + name);
237            if (ctrl.type === 'checkbox') {
238                ctrl.checked = value;
239
240            } else if (typeof ctrl.options !== 'undefined') {
241                for (var i = 0; i < ctrl.options.length; i += 1) {
242                    if (ctrl.options[i].value === value) {
243                        ctrl.selectedIndex = i;
244                        break;
245                    }
246                }
247            } else {
248                /*Weird IE9 error leads to 'null' appearring
249                in textboxes instead of ''.*/
250                if (value === null) {
251                    value = "";
252                }
253                ctrl.value = value;
254            }
255        },
256
257        // Save control setting to cookie
258        saveSetting: function(name) {
259            var val, ctrl = $D('noVNC_' + name);
260            if (ctrl.type === 'checkbox') {
261                val = ctrl.checked;
262            } else if (typeof ctrl.options !== 'undefined') {
263                val = ctrl.options[ctrl.selectedIndex].value;
264            } else {
265                val = ctrl.value;
266            }
267            WebUtil.writeSetting(name, val);
268            //Util.Debug("Setting saved '" + name + "=" + val + "'");
269            return val;
270        },
271
272        // Initial page load read/initialization of settings
273        initSetting: function(name, defVal) {
274            // Check Query string followed by cookie
275            var val = WebUtil.getQueryVar(name);
276            if (val === null) {
277                val = WebUtil.readSetting(name, defVal);
278            }
279            UI.updateSetting(name, val);
280            return val;
281        },
282
283        // Force a setting to be a certain value
284        forceSetting: function(name, val) {
285            UI.updateSetting(name, val);
286            return val;
287        },
288
289
290        // Show the popup status panel
291        togglePopupStatusPanel: function() {
292            var psp = $D('noVNC_popup_status_panel');
293            if (UI.popupStatusOpen === true) {
294                psp.style.display = "none";
295                UI.popupStatusOpen = false;
296            } else {
297                psp.innerHTML = $D('noVNC_status').innerHTML;
298                psp.style.display = "block";
299                psp.style.left = window.innerWidth/2 -
300                    parseInt(window.getComputedStyle(psp, false).width)/2 -30 + "px";
301                UI.popupStatusOpen = true;
302            }
303        },
304
305        // Show the XVP panel
306        toggleXvpPanel: function() {
307            // Close the description panel
308            $D('noVNC_description').style.display = "none";
309            // Close settings if open
310            if (UI.settingsOpen === true) {
311                UI.settingsApply();
312                UI.closeSettingsMenu();
313            }
314            // Close connection settings if open
315            if (UI.connSettingsOpen === true) {
316                UI.toggleConnectPanel();
317            }
318            // Close popup status panel if open
319            if (UI.popupStatusOpen === true) {
320                UI.togglePopupStatusPanel();
321            }
322            // Close clipboard panel if open
323            if (UI.clipboardOpen === true) {
324                UI.toggleClipboardPanel();
325            }
326            // Toggle XVP panel
327            if (UI.xvpOpen === true) {
328                $D('noVNC_xvp').style.display = "none";
329                $D('xvpButton').className = "noVNC_status_button";
330                UI.xvpOpen = false;
331            } else {
332                $D('noVNC_xvp').style.display = "block";
333                $D('xvpButton').className = "noVNC_status_button_selected";
334                UI.xvpOpen = true;
335            }
336        },
337
338        // Show the clipboard panel
339        toggleClipboardPanel: function() {
340            // Close the description panel
341            $D('noVNC_description').style.display = "none";
342            // Close settings if open
343            if (UI.settingsOpen === true) {
344                UI.settingsApply();
345                UI.closeSettingsMenu();
346            }
347            // Close connection settings if open
348            if (UI.connSettingsOpen === true) {
349                UI.toggleConnectPanel();
350            }
351            // Close popup status panel if open
352            if (UI.popupStatusOpen === true) {
353                UI.togglePopupStatusPanel();
354            }
355            // Close XVP panel if open
356            if (UI.xvpOpen === true) {
357                UI.toggleXvpPanel();
358            }
359            // Toggle Clipboard Panel
360            if (UI.clipboardOpen === true) {
361                $D('noVNC_clipboard').style.display = "none";
362                $D('clipboardButton').className = "noVNC_status_button";
363                UI.clipboardOpen = false;
364            } else {
365                $D('noVNC_clipboard').style.display = "block";
366                $D('clipboardButton').className = "noVNC_status_button_selected";
367                UI.clipboardOpen = true;
368            }
369        },
370
371        // Show the connection settings panel/menu
372        toggleConnectPanel: function() {
373            // Close the description panel
374            $D('noVNC_description').style.display = "none";
375            // Close connection settings if open
376            if (UI.settingsOpen === true) {
377                UI.settingsApply();
378                UI.closeSettingsMenu();
379                $D('connectButton').className = "noVNC_status_button";
380            }
381            // Close clipboard panel if open
382            if (UI.clipboardOpen === true) {
383                UI.toggleClipboardPanel();
384            }
385            // Close popup status panel if open
386            if (UI.popupStatusOpen === true) {
387                UI.togglePopupStatusPanel();
388            }
389            // Close XVP panel if open
390            if (UI.xvpOpen === true) {
391                UI.toggleXvpPanel();
392            }
393
394            // Toggle Connection Panel
395            if (UI.connSettingsOpen === true) {
396                $D('noVNC_controls').style.display = "none";
397                $D('connectButton').className = "noVNC_status_button";
398                UI.connSettingsOpen = false;
399                UI.saveSetting('host');
400                UI.saveSetting('port');
401                //UI.saveSetting('password');
402            } else {
403                $D('noVNC_controls').style.display = "block";
404                $D('connectButton').className = "noVNC_status_button_selected";
405                UI.connSettingsOpen = true;
406                $D('noVNC_host').focus();
407            }
408        },
409
410        // Toggle the settings menu:
411        //   On open, settings are refreshed from saved cookies.
412        //   On close, settings are applied
413        toggleSettingsPanel: function() {
414            // Close the description panel
415            $D('noVNC_description').style.display = "none";
416            if (UI.settingsOpen) {
417                UI.settingsApply();
418                UI.closeSettingsMenu();
419            } else {
420                UI.updateSetting('encrypt');
421                UI.updateSetting('true_color');
422                if (UI.rfb.get_display().get_cursor_uri()) {
423                    UI.updateSetting('cursor');
424                } else {
425                    UI.updateSetting('cursor', !UI.isTouchDevice);
426                    $D('noVNC_cursor').disabled = true;
427                }
428                UI.updateSetting('clip');
429                UI.updateSetting('shared');
430                UI.updateSetting('view_only');
431                UI.updateSetting('path');
432                UI.updateSetting('repeaterID');
433                UI.updateSetting('stylesheet');
434                UI.updateSetting('logging');
435
436                UI.openSettingsMenu();
437            }
438        },
439
440        // Open menu
441        openSettingsMenu: function() {
442            // Close the description panel
443            $D('noVNC_description').style.display = "none";
444            // Close clipboard panel if open
445            if (UI.clipboardOpen === true) {
446                UI.toggleClipboardPanel();
447            }
448            // Close connection settings if open
449            if (UI.connSettingsOpen === true) {
450                UI.toggleConnectPanel();
451            }
452            // Close popup status panel if open
453            if (UI.popupStatusOpen === true) {
454                UI.togglePopupStatusPanel();
455            }
456            // Close XVP panel if open
457            if (UI.xvpOpen === true) {
458                UI.toggleXvpPanel();
459            }
460            $D('noVNC_settings').style.display = "block";
461            $D('settingsButton').className = "noVNC_status_button_selected";
462            UI.settingsOpen = true;
463        },
464
465        // Close menu (without applying settings)
466        closeSettingsMenu: function() {
467            $D('noVNC_settings').style.display = "none";
468            $D('settingsButton').className = "noVNC_status_button";
469            UI.settingsOpen = false;
470        },
471
472        // Save/apply settings when 'Apply' button is pressed
473        settingsApply: function() {
474            //Util.Debug(">> settingsApply");
475            UI.saveSetting('encrypt');
476            UI.saveSetting('true_color');
477            if (UI.rfb.get_display().get_cursor_uri()) {
478                UI.saveSetting('cursor');
479            }
480            UI.saveSetting('clip');
481            UI.saveSetting('shared');
482            UI.saveSetting('view_only');
483            UI.saveSetting('path');
484            UI.saveSetting('repeaterID');
485            UI.saveSetting('stylesheet');
486            UI.saveSetting('logging');
487
488            // Settings with immediate (non-connected related) effect
489            WebUtil.selectStylesheet(UI.getSetting('stylesheet'));
490            WebUtil.init_logging(UI.getSetting('logging'));
491            UI.setViewClip();
492            UI.setViewDrag(UI.rfb.get_viewportDrag());
493            //Util.Debug("<< settingsApply");
494        },
495
496
497
498        setPassword: function() {
499            UI.rfb.sendPassword($D('noVNC_password').value);
500            //Reset connect button.
501            $D('noVNC_connect_button').value = "Connect";
502            $D('noVNC_connect_button').onclick = UI.Connect;
503            //Hide connection panel.
504            UI.toggleConnectPanel();
505            return false;
506        },
507
508        sendCtrlAltDel: function() {
509            UI.rfb.sendCtrlAltDel();
510        },
511
512        xvpShutdown: function() {
513            UI.rfb.xvpShutdown();
514        },
515
516        xvpReboot: function() {
517            UI.rfb.xvpReboot();
518        },
519
520        xvpReset: function() {
521            UI.rfb.xvpReset();
522        },
523
524        setMouseButton: function(num) {
525            if (typeof num === 'undefined') {
526                // Disable mouse buttons
527                num = -1;
528            }
529            if (UI.rfb) {
530                UI.rfb.get_mouse().set_touchButton(num);
531            }
532
533            var blist = [0, 1,2,4];
534            for (var b = 0; b < blist.length; b++) {
535                var button = $D('noVNC_mouse_button' + blist[b]);
536                if (blist[b] === num) {
537                    button.style.display = "";
538                } else {
539                    button.style.display = "none";
540                }
541            }
542        },
543
544        updateState: function(rfb, state, oldstate, msg) {
545            UI.rfb_state = state;
546            var klass;
547            switch (state) {
548                case 'failed':
549                case 'fatal':
550                    klass = "noVNC_status_error";
551                    break;
552                case 'normal':
553                    klass = "noVNC_status_normal";
554                    break;
555                case 'disconnected':
556                    $D('noVNC_logo').style.display = "block";
557                    /* falls through */
558                case 'loaded':
559                    klass = "noVNC_status_normal";
560                    break;
561                case 'password':
562                    UI.toggleConnectPanel();
563
564                    $D('noVNC_connect_button').value = "Send Password";
565                    $D('noVNC_connect_button').onclick = UI.setPassword;
566                    $D('noVNC_password').focus();
567
568                    klass = "noVNC_status_warn";
569                    break;
570                default:
571                    klass = "noVNC_status_warn";
572                    break;
573            }
574
575            if (typeof(msg) !== 'undefined') {
576                $D('noVNC-control-bar').setAttribute("class", klass);
577                $D('noVNC_status').innerHTML = msg;
578            }
579
580            UI.updateVisualState();
581        },
582
583        // Disable/enable controls depending on connection state
584        updateVisualState: function() {
585            var connected = UI.rfb_state === 'normal' ? true : false;
586
587            //Util.Debug(">> updateVisualState");
588            $D('noVNC_encrypt').disabled = connected;
589            $D('noVNC_true_color').disabled = connected;
590            if (UI.rfb && UI.rfb.get_display() &&
591                UI.rfb.get_display().get_cursor_uri()) {
592                $D('noVNC_cursor').disabled = connected;
593            } else {
594                UI.updateSetting('cursor', !UI.isTouchDevice);
595                $D('noVNC_cursor').disabled = true;
596            }
597            $D('noVNC_shared').disabled = connected;
598            $D('noVNC_view_only').disabled = connected;
599            $D('noVNC_path').disabled = connected;
600            $D('noVNC_repeaterID').disabled = connected;
601
602            if (connected) {
603                UI.setViewClip();
604                UI.setMouseButton(1);
605                $D('clipboardButton').style.display = "inline";
606                $D('showKeyboard').style.display = "inline";
607                $D('noVNC_extra_keys').style.display = "";
608                $D('sendCtrlAltDelButton').style.display = "inline";
609            } else {
610                UI.setMouseButton();
611                $D('clipboardButton').style.display = "none";
612                $D('showKeyboard').style.display = "none";
613                $D('noVNC_extra_keys').style.display = "none";
614                $D('sendCtrlAltDelButton').style.display = "none";
615                UI.updateXvpVisualState(0);
616            }
617
618            // State change disables viewport dragging.
619            // It is enabled (toggled) by direct click on the button
620            UI.setViewDrag(false);
621
622            switch (UI.rfb_state) {
623                case 'fatal':
624                case 'failed':
625                case 'loaded':
626                case 'disconnected':
627                    $D('connectButton').style.display = "";
628                    $D('disconnectButton').style.display = "none";
629                    break;
630                default:
631                    $D('connectButton').style.display = "none";
632                    $D('disconnectButton').style.display = "";
633                    break;
634            }
635
636            //Util.Debug("<< updateVisualState");
637        },
638
639        // Disable/enable XVP button
640        updateXvpVisualState: function(ver) {
641            if (ver >= 1) {
642                $D('xvpButton').style.display = 'inline';
643            } else {
644                $D('xvpButton').style.display = 'none';
645                // Close XVP panel if open
646                if (UI.xvpOpen === true) {
647                    UI.toggleXvpPanel();
648                }
649            }
650        },
651
652        // Display the desktop name in the document title
653        updateDocumentTitle: function(rfb, name) {
654            document.title = name + " - noVNC";
655        },
656
657        clipReceive: function(rfb, text) {
658            Util.Debug(">> UI.clipReceive: " + text.substr(0,40) + "...");
659            $D('noVNC_clipboard_text').value = text;
660            Util.Debug("<< UI.clipReceive");
661        },
662
663        connect: function() {
664            UI.closeSettingsMenu();
665            UI.toggleConnectPanel();
666
667            var host = $D('noVNC_host').value;
668            var port = $D('noVNC_port').value;
669            var password = $D('noVNC_password').value;
670            var path = $D('noVNC_path').value;
671            if ((!host) || (!port)) {
672                throw new Error("Must set host and port");
673            }
674
675            UI.rfb.set_encrypt(UI.getSetting('encrypt'));
676            UI.rfb.set_true_color(UI.getSetting('true_color'));
677            UI.rfb.set_local_cursor(UI.getSetting('cursor'));
678            UI.rfb.set_shared(UI.getSetting('shared'));
679            UI.rfb.set_view_only(UI.getSetting('view_only'));
680            UI.rfb.set_repeaterID(UI.getSetting('repeaterID'));
681
682            UI.rfb.connect(host, port, password, path);
683
684            //Close dialog.
685            setTimeout(UI.setBarPosition, 100);
686            $D('noVNC_logo').style.display = "none";
687        },
688
689        disconnect: function() {
690            UI.closeSettingsMenu();
691            UI.rfb.disconnect();
692
693            $D('noVNC_logo').style.display = "block";
694            UI.connSettingsOpen = false;
695            UI.toggleConnectPanel();
696        },
697
698        displayBlur: function() {
699            UI.rfb.get_keyboard().set_focused(false);
700            UI.rfb.get_mouse().set_focused(false);
701        },
702
703        displayFocus: function() {
704            UI.rfb.get_keyboard().set_focused(true);
705            UI.rfb.get_mouse().set_focused(true);
706        },
707
708        clipClear: function() {
709            $D('noVNC_clipboard_text').value = "";
710            UI.rfb.clipboardPasteFrom("");
711        },
712
713        clipSend: function() {
714            var text = $D('noVNC_clipboard_text').value;
715            Util.Debug(">> UI.clipSend: " + text.substr(0,40) + "...");
716            UI.rfb.clipboardPasteFrom(text);
717            Util.Debug("<< UI.clipSend");
718        },
719
720        // Enable/disable and configure viewport clipping
721        setViewClip: function(clip) {
722            var display;
723            if (UI.rfb) {
724                display = UI.rfb.get_display();
725            } else {
726                return;
727            }
728
729            var cur_clip = display.get_viewport();
730
731            if (typeof(clip) !== 'boolean') {
732                // Use current setting
733                clip = UI.getSetting('clip');
734            }
735
736            if (clip && !cur_clip) {
737                // Turn clipping on
738                UI.updateSetting('clip', true);
739            } else if (!clip && cur_clip) {
740                // Turn clipping off
741                UI.updateSetting('clip', false);
742                display.set_viewport(false);
743                $D('noVNC_canvas').style.position = 'static';
744                display.viewportChange();
745            }
746            if (UI.getSetting('clip')) {
747                // If clipping, update clipping settings
748                $D('noVNC_canvas').style.position = 'absolute';
749                var pos = Util.getPosition($D('noVNC_canvas'));
750                var new_w = window.innerWidth - pos.x;
751                var new_h = window.innerHeight - pos.y;
752                display.set_viewport(true);
753                display.viewportChange(0, 0, new_w, new_h);
754            }
755        },
756
757        // Toggle/set/unset the viewport drag/move button
758        setViewDrag: function(drag) {
759            var vmb = $D('noVNC_view_drag_button');
760            if (!UI.rfb) { return; }
761
762            if (UI.rfb_state === 'normal' &&
763                UI.rfb.get_display().get_viewport()) {
764                vmb.style.display = "inline";
765            } else {
766                vmb.style.display = "none";
767            }
768
769            if (typeof(drag) === "undefined" ||
770                typeof(drag) === "object") {
771                // If not specified, then toggle
772                drag = !UI.rfb.get_viewportDrag();
773            }
774            if (drag) {
775                vmb.className = "noVNC_status_button_selected";
776                UI.rfb.set_viewportDrag(true);
777            } else {
778                vmb.className = "noVNC_status_button";
779                UI.rfb.set_viewportDrag(false);
780            }
781        },
782
783        // On touch devices, show the OS keyboard
784        showKeyboard: function() {
785            var kbi = $D('keyboardinput');
786            var skb = $D('showKeyboard');
787            var l = kbi.value.length;
788            if(UI.keyboardVisible === false) {
789                kbi.focus();
790                try { kbi.setSelectionRange(l, l); } // Move the caret to the end
791                catch (err) {} // setSelectionRange is undefined in Google Chrome
792                UI.keyboardVisible = true;
793                skb.className = "noVNC_status_button_selected";
794            } else if(UI.keyboardVisible === true) {
795                kbi.blur();
796                skb.className = "noVNC_status_button";
797                UI.keyboardVisible = false;
798            }
799        },
800
801        keepKeyboard: function() {
802            clearTimeout(UI.hideKeyboardTimeout);
803            if(UI.keyboardVisible === true) {
804                $D('keyboardinput').focus();
805                $D('showKeyboard').className = "noVNC_status_button_selected";
806            } else if(UI.keyboardVisible === false) {
807                $D('keyboardinput').blur();
808                $D('showKeyboard').className = "noVNC_status_button";
809            }
810        },
811
812        keyboardinputReset: function() {
813            var kbi = $D('keyboardinput');
814            kbi.value = new Array(UI.defaultKeyboardinputLen).join("_");
815            UI.lastKeyboardinput = kbi.value;
816        },
817
818        // When normal keyboard events are left uncought, use the input events from
819        // the keyboardinput element instead and generate the corresponding key events.
820        // This code is required since some browsers on Android are inconsistent in
821        // sending keyCodes in the normal keyboard events when using on screen keyboards.
822        keyInput: function(event) {
823            var newValue = event.target.value;
824            var oldValue = UI.lastKeyboardinput;
825
826            var newLen;
827            try {
828                // Try to check caret position since whitespace at the end
829                // will not be considered by value.length in some browsers
830                newLen = Math.max(event.target.selectionStart, newValue.length);
831            } catch (err) {
832                // selectionStart is undefined in Google Chrome
833                newLen = newValue.length;
834            }
835            var oldLen = oldValue.length;
836
837            var backspaces;
838            var inputs = newLen - oldLen;
839            if (inputs < 0) {
840                backspaces = -inputs;
841            } else {
842                backspaces = 0;
843            }
844
845            // Compare the old string with the new to account for
846            // text-corrections or other input that modify existing text
847            var i;
848            for (i = 0; i < Math.min(oldLen, newLen); i++) {
849                if (newValue.charAt(i) != oldValue.charAt(i)) {
850                    inputs = newLen - i;
851                    backspaces = oldLen - i;
852                    break;
853                }
854            }
855
856            // Send the key events
857            for (i = 0; i < backspaces; i++) {
858                UI.rfb.sendKey(XK_BackSpace);
859            }
860            for (i = newLen - inputs; i < newLen; i++) {
861                UI.rfb.sendKey(newValue.charCodeAt(i));
862            }
863
864            // Control the text content length in the keyboardinput element
865            if (newLen > 2 * UI.defaultKeyboardinputLen) {
866                UI.keyboardinputReset();
867            } else if (newLen < 1) {
868                // There always have to be some text in the keyboardinput
869                // element with which backspace can interact.
870                UI.keyboardinputReset();
871                // This sometimes causes the keyboard to disappear for a second
872                // but it is required for the android keyboard to recognize that
873                // text has been added to the field
874                event.target.blur();
875                // This has to be ran outside of the input handler in order to work
876                setTimeout(function() { UI.keepKeyboard(); }, 0);
877            } else {
878                UI.lastKeyboardinput = newValue;
879            }
880        },
881
882        keyInputBlur: function() {
883            $D('showKeyboard').className = "noVNC_status_button";
884            //Weird bug in iOS if you change keyboardVisible
885            //here it does not actually occur so next time
886            //you click keyboard icon it doesnt work.
887            UI.hideKeyboardTimeout = setTimeout(function() { UI.setKeyboard(); },100);
888        },
889
890        showExtraKeys: function() {
891            UI.keepKeyboard();
892            if(UI.extraKeysVisible === false) {
893                $D('toggleCtrlButton').style.display = "inline";
894                $D('toggleAltButton').style.display = "inline";
895                $D('sendTabButton').style.display = "inline";
896                $D('sendEscButton').style.display = "inline";
897                $D('showExtraKeysButton').className = "noVNC_status_button_selected";
898                UI.extraKeysVisible = true;
899            } else if(UI.extraKeysVisible === true) {
900                $D('toggleCtrlButton').style.display = "";
901                $D('toggleAltButton').style.display = "";
902                $D('sendTabButton').style.display = "";
903                $D('sendEscButton').style.display = "";
904                $D('showExtraKeysButton').className = "noVNC_status_button";
905                UI.extraKeysVisible = false;
906            }
907        },
908
909        toggleCtrl: function() {
910            UI.keepKeyboard();
911            if(UI.ctrlOn === false) {
912                UI.rfb.sendKey(XK_Control_L, true);
913                $D('toggleCtrlButton').className = "noVNC_status_button_selected";
914                UI.ctrlOn = true;
915            } else if(UI.ctrlOn === true) {
916                UI.rfb.sendKey(XK_Control_L, false);
917                $D('toggleCtrlButton').className = "noVNC_status_button";
918                UI.ctrlOn = false;
919            }
920        },
921
922        toggleAlt: function() {
923            UI.keepKeyboard();
924            if(UI.altOn === false) {
925                UI.rfb.sendKey(XK_Alt_L, true);
926                $D('toggleAltButton').className = "noVNC_status_button_selected";
927                UI.altOn = true;
928            } else if(UI.altOn === true) {
929                UI.rfb.sendKey(XK_Alt_L, false);
930                $D('toggleAltButton').className = "noVNC_status_button";
931                UI.altOn = false;
932            }
933        },
934
935        sendTab: function() {
936            UI.keepKeyboard();
937            UI.rfb.sendKey(XK_Tab);
938        },
939
940        sendEsc: function() {
941            UI.keepKeyboard();
942            UI.rfb.sendKey(XK_Escape);
943        },
944
945        setKeyboard: function() {
946            UI.keyboardVisible = false;
947        },
948
949        // iOS < Version 5 does not support position fixed. Javascript workaround:
950        setOnscroll: function() {
951            window.onscroll = function() {
952                UI.setBarPosition();
953            };
954        },
955
956        setResize: function () {
957            window.onResize = function() {
958                UI.setBarPosition();
959            };
960        },
961
962        //Helper to add options to dropdown.
963        addOption: function(selectbox, text, value) {
964            var optn = document.createElement("OPTION");
965            optn.text = text;
966            optn.value = value;
967            selectbox.options.add(optn);
968        },
969
970        setBarPosition: function() {
971            $D('noVNC-control-bar').style.top = (window.pageYOffset) + 'px';
972            $D('noVNC_mobile_buttons').style.left = (window.pageXOffset) + 'px';
973
974            var vncwidth = $D('noVNC_screen').style.offsetWidth;
975            $D('noVNC-control-bar').style.width = vncwidth + 'px';
976        }
977
978    };
979})();
980