88bfff5b55aaacbcb09e744fdac2dbe9846bf655
[guacamole.git] / src / main / webapp / scripts / interface.js
1
2 // UI Definition
3 var GuacamoleUI = {
4
5     "viewport"    : document.getElementById("viewportClone"),
6     "display"     : document.getElementById("display"),
7     "menu"        : document.getElementById("menu"),
8     "menuControl" : document.getElementById("menuControl"),
9     "touchMenu"   : document.getElementById("touchMenu"),
10     "logo"        : document.getElementById("status-logo"),
11     "eventTarget" : document.getElementById("eventTarget"),
12
13     "buttons": {
14
15         "showClipboard": document.getElementById("showClipboard"),
16         "showKeyboard" : document.getElementById("showKeyboard"),
17         "ctrlAltDelete": document.getElementById("ctrlAltDelete"),
18         "reconnect"    : document.getElementById("reconnect"),
19         "logout"       : document.getElementById("logout")
20
21     },
22
23     "containers": {
24         "state"    : document.getElementById("statusDialog"),
25         "clipboard": document.getElementById("clipboardDiv"),
26         "keyboard" : document.getElementById("keyboardContainer")
27     },
28     
29     "state"     : document.getElementById("statusText"),
30     "clipboard" : document.getElementById("clipboard")
31
32 };
33
34 // Constant UI initialization and behavior
35 (function() {
36
37     var menu_shaded = false;
38
39     var shade_interval = null;
40     var show_interval = null;
41
42     // Cache error image (might not be available when error occurs)
43     var guacErrorImage = new Image();
44     guacErrorImage.src = "images/noguacamole-logo-24.png";
45
46     // Function for adding a class to an element
47     var addClass;
48
49     // Function for removing a class from an element
50     var removeClass;
51
52     // If Node.classList is supported, implement addClass/removeClass using that
53     if (Node.classList) {
54
55         addClass = function(element, classname) {
56             element.classList.add(classname);
57         };
58         
59         removeClass = function(element, classname) {
60             element.classList.remove(classname);
61         };
62         
63     }
64
65     // Otherwise, implement own
66     else {
67
68         addClass = function(element, classname) {
69
70             // Simply add new class
71             element.className += " " + classname;
72
73         };
74         
75         removeClass = function(element, classname) {
76
77             // Filter out classes with given name
78             element.className = element.className.replace(/([^ ]+)[ ]*/g,
79                 function(match, testClassname, spaces, offset, string) {
80
81                     // If same class, remove
82                     if (testClassname == classname)
83                         return "";
84
85                     // Otherwise, allow
86                     return match;
87                     
88                 }
89             );
90
91         };
92         
93     }
94
95
96     GuacamoleUI.hideStatus = function() {
97         removeClass(document.body, "guac-error");
98         GuacamoleUI.containers.state.style.visibility = "hidden";
99         GuacamoleUI.display.style.opacity = "1";
100     };
101     
102     GuacamoleUI.showStatus = function(text) {
103         removeClass(document.body, "guac-error");
104         GuacamoleUI.containers.state.style.visibility = "visible";
105         GuacamoleUI.state.textContent = text;
106         GuacamoleUI.display.style.opacity = "1";
107     };
108     
109     GuacamoleUI.showError = function(error) {
110         addClass(document.body, "guac-error");
111         GuacamoleUI.state.textContent = error;
112         GuacamoleUI.display.style.opacity = "0.1";
113     };
114
115     GuacamoleUI.shadeMenu = function() {
116
117         if (!menu_shaded) {
118
119             var step = Math.floor(GuacamoleUI.menu.offsetHeight / 10) + 1;
120             var offset = 0;
121             menu_shaded = true;
122
123             window.clearInterval(show_interval);
124             shade_interval = window.setInterval(function() {
125
126                 offset -= step;
127                 GuacamoleUI.menu.style.top = offset + "px";
128
129                 if (offset <= -GuacamoleUI.menu.offsetHeight) {
130                     window.clearInterval(shade_interval);
131                     GuacamoleUI.menu.style.visiblity = "hidden";
132                 }
133
134             }, 30);
135         }
136
137     };
138
139     GuacamoleUI.showMenu = function() {
140
141         if (menu_shaded) {
142
143             var step = Math.floor(GuacamoleUI.menu.offsetHeight / 5) + 1;
144             var offset = -GuacamoleUI.menu.offsetHeight;
145             menu_shaded = false;
146             GuacamoleUI.menu.style.visiblity = "";
147
148             window.clearInterval(shade_interval);
149             show_interval = window.setInterval(function() {
150
151                 offset += step;
152
153                 if (offset >= 0) {
154                     offset = 0;
155                     window.clearInterval(show_interval);
156                 }
157
158                 GuacamoleUI.menu.style.top = offset + "px";
159
160             }, 30);
161         }
162
163     };
164
165     // Show/Hide clipboard
166     GuacamoleUI.buttons.showClipboard.onclick = function() {
167
168         var displayed = GuacamoleUI.containers.clipboard.style.display;
169         if (displayed != "block") {
170             GuacamoleUI.containers.clipboard.style.display = "block";
171             GuacamoleUI.buttons.showClipboard.innerHTML = "Hide Clipboard";
172         }
173         else {
174             GuacamoleUI.containers.clipboard.style.display = "none";
175             GuacamoleUI.buttons.showClipboard.innerHTML = "Show Clipboard";
176             GuacamoleUI.clipboard.onchange();
177         }
178
179     };
180
181     /**
182      * When GuacamoleUI.oskMode == OSK_MODE_NATIVE, "Show Keyboard" tries
183      * to use the native OSK instead of the Guacamole OSK.
184      */
185     GuacamoleUI.OSK_MODE_NATIVE = 1;
186
187     /**
188      * When GuacamoleUI.oskMode == OSK_MODE_GUAC, "Show Keyboard" uses the 
189      * Guacamole OSK, regardless of whether a native OSK is available.
190      */
191     GuacamoleUI.OSK_MODE_GUAC   = 2;
192
193     // Assume no native OSK by default
194     GuacamoleUI.oskMode = GuacamoleUI.OSK_MODE_GUAC;
195
196     // Show/Hide keyboard
197     var keyboardResizeInterval = null;
198     GuacamoleUI.buttons.showKeyboard.onclick = function() {
199
200         // If Guac OSK shown, hide it.
201         var displayed = GuacamoleUI.containers.keyboard.style.display;
202         if (displayed == "block") {
203             GuacamoleUI.containers.keyboard.style.display = "none";
204             GuacamoleUI.buttons.showKeyboard.textContent = "Show Keyboard";
205
206             window.onresize = null;
207             window.clearInterval(keyboardResizeInterval);
208         }
209         
210         // If not shown ... action depends on OSK mode.
211         else {
212
213             // If we think the platform has a native OSK, use the event target to
214             // cause it to display.
215             if (GuacamoleUI.oskMode == GuacamoleUI.OSK_MODE_NATIVE) {
216
217                 // ...but use the Guac OSK if clicked again
218                 GuacamoleUI.oskMode = GuacamoleUI.OSK_MODE_GUAC;
219
220                 // Try to show native OSK by focusing eventTarget.
221                 GuacamoleUI.eventTarget.focus();
222                 return;
223
224             }
225
226             // Ensure event target is NOT focused if we are using the Guac OSK.
227             GuacamoleUI.eventTarget.blur();
228
229             GuacamoleUI.containers.keyboard.style.display = "block";
230             GuacamoleUI.buttons.showKeyboard.textContent = "Hide Keyboard";
231
232             // Automatically update size
233             window.onresize = updateKeyboardSize;
234             keyboardResizeInterval = window.setInterval(updateKeyboardSize, 30);
235
236             updateKeyboardSize();
237         }
238         
239
240     };
241
242     // Logout
243     GuacamoleUI.buttons.logout.onclick = function() {
244         window.location.href = "logout";
245     };
246
247     // Timeouts for detecting if users wants menu to open or close
248     var detectMenuOpenTimeout = null;
249     var detectMenuCloseTimeout = null;
250
251     // Clear detection timeouts
252     GuacamoleUI.resetMenuDetect = function() {
253
254         if (detectMenuOpenTimeout != null) {
255             window.clearTimeout(detectMenuOpenTimeout);
256             detectMenuOpenTimeout = null;
257         }
258
259         if (detectMenuCloseTimeout != null) {
260             window.clearTimeout(detectMenuCloseTimeout);
261             detectMenuCloseTimeout = null;
262         }
263
264     };
265
266     // Initiate detection of menu open action. If not canceled through some
267     // user event, menu will open.
268     GuacamoleUI.startMenuOpenDetect = function() {
269
270         if (!detectMenuOpenTimeout) {
271
272             // Clear detection state
273             GuacamoleUI.resetMenuDetect();
274
275             // Wait and then show menu
276             detectMenuOpenTimeout = window.setTimeout(function() {
277
278                 // If menu opened via mouse, do not show native OSK
279                 GuacamoleUI.oskMode = GuacamoleUI.OSK_MODE_GUAC;
280
281                 GuacamoleUI.showMenu();
282                 detectMenuOpenTimeout = null;
283             }, 325);
284
285         }
286
287     };
288
289     // Initiate detection of menu close action. If not canceled through some
290     // user mouse event, menu will close.
291     GuacamoleUI.startMenuCloseDetect = function() {
292
293         if (!detectMenuCloseTimeout) {
294
295             // Clear detection state
296             GuacamoleUI.resetMenuDetect();
297
298             // Wait and then shade menu
299             detectMenuCloseTimeout = window.setTimeout(function() {
300                 GuacamoleUI.shadeMenu();
301                 detectMenuCloseTimeout = null;
302             }, 500);
303
304         }
305
306     };
307
308     // Show menu if mouseover any part of menu
309     GuacamoleUI.menu.addEventListener('mouseover', GuacamoleUI.showMenu, true);
310
311     // Stop detecting menu state change intents if mouse is over menu
312     GuacamoleUI.menu.addEventListener('mouseover', GuacamoleUI.resetMenuDetect, true);
313
314     // When mouse hovers over top of screen, start detection of intent to open menu
315     GuacamoleUI.menuControl.addEventListener('mousemove', GuacamoleUI.startMenuOpenDetect, true);
316
317     var long_press_start_x = 0;
318     var long_press_start_y = 0;
319     var menuShowLongPressTimeout = null;
320
321     GuacamoleUI.startLongPressDetect = function() {
322
323         if (!menuShowLongPressTimeout) {
324
325             menuShowLongPressTimeout = window.setTimeout(function() {
326                 
327                 menuShowLongPressTimeout = null;
328
329                 // Assume native OSK if menu shown via long-press
330                 GuacamoleUI.oskMode = GuacamoleUI.OSK_MODE_NATIVE;
331                 GuacamoleUI.showMenu();
332
333             }, 800);
334
335         }
336     };
337
338     GuacamoleUI.stopLongPressDetect = function() {
339         window.clearTimeout(menuShowLongPressTimeout);
340         menuShowLongPressTimeout = null;
341     };
342
343     // Reset event target (add content, reposition cursor in middle.
344     GuacamoleUI.resetEventTarget = function() {
345         GuacamoleUI.eventTarget.value = "GUAC";
346         GuacamoleUI.eventTarget.selectionStart =
347         GuacamoleUI.eventTarget.selectionEnd   = 2;
348     };
349
350     // Detect long-press at bottom of screen
351     GuacamoleUI.display.addEventListener('touchstart', function(e) {
352         
353         // Close menu if shown
354         GuacamoleUI.shadeMenu();
355         
356         // Record touch location
357         if (e.touches.length == 1) {
358             var touch = e.touches[0];
359             long_press_start_x = touch.pageX;
360             long_press_start_y = touch.pageY;
361         }
362         
363         // Start detection
364         GuacamoleUI.startLongPressDetect();
365         
366     }, true);
367
368     // Stop detection if touch moves significantly
369     GuacamoleUI.display.addEventListener('touchmove', function(e) {
370         
371         if (e.touches.length == 1) {
372
373             // If touch distance from start exceeds threshold, cancel long press
374             var touch = e.touches[0];
375             if (Math.abs(touch.pageX - long_press_start_x) >= 10
376                 || Math.abs(touch.pageY - long_press_start_y) >= 10)
377                 GuacamoleUI.stopLongPressDetect();
378
379         }
380         
381     }, true);
382
383     // Stop detection if press stops
384     GuacamoleUI.display.addEventListener('touchend', GuacamoleUI.stopLongPressDetect, true);
385
386     // Close menu on mouse movement
387     GuacamoleUI.display.addEventListener('mousemove', GuacamoleUI.startMenuCloseDetect, true);
388     GuacamoleUI.display.addEventListener('mousedown', GuacamoleUI.startMenuCloseDetect, true);
389
390     // Reconnect button
391     GuacamoleUI.buttons.reconnect.onclick = function() {
392         window.location.reload();
393     };
394
395     // On-screen keyboard
396     GuacamoleUI.keyboard = new Guacamole.OnScreenKeyboard("layouts/en-us-qwerty-mobile.xml");
397     GuacamoleUI.containers.keyboard.appendChild(GuacamoleUI.keyboard.getElement());
398
399     // Function for automatically updating keyboard size
400     var lastKeyboardWidth;
401     function updateKeyboardSize() {
402         var currentSize = GuacamoleUI.keyboard.getElement().offsetWidth;
403         if (lastKeyboardWidth != currentSize) {
404             GuacamoleUI.keyboard.resize(currentSize);
405             lastKeyboardWidth = currentSize;
406         }
407     };
408
409 })();
410
411 // Tie UI events / behavior to a specific Guacamole client
412 GuacamoleUI.attach = function(guac) {
413
414     var title_prefix = null;
415     var connection_name = null 
416     
417     var guac_display = guac.getDisplay();
418
419     // Set document title appropriately, based on prefix and connection name
420     function updateTitle() {
421
422         // Use title prefix if present
423         if (title_prefix) {
424             
425             document.title = title_prefix;
426
427             // Include connection name, if present
428             if (connection_name)
429                 document.title += " " + connection_name;
430
431         }
432
433         // Otherwise, just set to connection name
434         else if (connection_name)
435             document.title = connection_name;
436
437     }
438
439     // When mouse enters display, start detection of intent to close menu
440     guac_display.addEventListener('mouseover', GuacamoleUI.startMenuCloseDetect, true);
441
442     guac_display.onclick = function(e) {
443         e.preventDefault();
444         return false;
445     };
446
447     // Mouse
448     var mouse = new Guacamole.Mouse(guac_display);
449     mouse.onmousedown = mouse.onmouseup = mouse.onmousemove =
450         function(mouseState) {
451        
452             // Determine mouse position within view
453             var mouse_view_x = mouseState.x + guac_display.offsetLeft - window.pageXOffset;
454             var mouse_view_y = mouseState.y + guac_display.offsetTop  - window.pageYOffset;
455
456             // Determine viewport dimensioins
457             var view_width  = GuacamoleUI.viewport.offsetWidth;
458             var view_height = GuacamoleUI.viewport.offsetHeight;
459
460             // Determine scroll amounts based on mouse position relative to document
461
462             var scroll_amount_x;
463             if (mouse_view_x > view_width)
464                 scroll_amount_x = mouse_view_x - view_width;
465             else if (mouse_view_x < 0)
466                 scroll_amount_x = mouse_view_x;
467             else
468                 scroll_amount_x = 0;
469
470             var scroll_amount_y;
471             if (mouse_view_y > view_height)
472                 scroll_amount_y = mouse_view_y - view_height;
473             else if (mouse_view_y < 0)
474                 scroll_amount_y = mouse_view_y;
475             else
476                 scroll_amount_y = 0;
477
478             // Scroll (if necessary) to keep mouse on screen.
479             window.scrollBy(scroll_amount_x, scroll_amount_y);
480        
481             // Send mouse event
482             guac.sendMouseState(mouseState);
483             
484         };
485
486     // Keyboard
487     var keyboard = new Guacamole.Keyboard(document);
488
489     function disableKeyboard() {
490         keyboard.onkeydown = null;
491         keyboard.onkeyup = null;
492     }
493
494     function enableKeyboard() {
495         keyboard.onkeydown = 
496             function (keysym) {
497           
498                 // If we're using native OSK, ensure event target is reset
499                 // on each key event.
500                 if (GuacamoleUI.oskMode == GuacamoleUI.OSK_MODE_NATIVE)
501                     GuacamoleUI.resetEventTarget();
502                 
503                 guac.sendKeyEvent(1, keysym);
504             };
505
506         keyboard.onkeyup = 
507             function (keysym) {
508                 guac.sendKeyEvent(0, keysym);
509             };
510     }
511
512     // Enable keyboard by default
513     enableKeyboard();
514
515     // Handle client state change
516     guac.onstatechange = function(clientState) {
517
518         switch (clientState) {
519
520             // Idle
521             case 0:
522                 GuacamoleUI.showStatus("Idle.");
523                 title_prefix = "[Idle]";
524                 break;
525
526             // Connecting
527             case 1:
528                 GuacamoleUI.shadeMenu();
529                 GuacamoleUI.showStatus("Connecting...");
530                 title_prefix = "[Connecting...]";
531                 break;
532
533             // Connected + waiting
534             case 2:
535                 GuacamoleUI.showStatus("Connected, waiting for first update...");
536                 title_prefix = "[Waiting...]";
537                 break;
538
539             // Connected
540             case 3:
541                 
542                 GuacamoleUI.hideStatus();
543                 GuacamoleUI.display.className =
544                     GuacamoleUI.display.className.replace(/guac-loading/, '');
545
546                 GuacamoleUI.menu.className = "connected";
547
548                 title_prefix = null;
549                 break;
550
551             // Disconnecting
552             case 4:
553                 GuacamoleUI.showStatus("Disconnecting...");
554                 title_prefix = "[Disconnecting...]";
555                 break;
556
557             // Disconnected
558             case 5:
559                 GuacamoleUI.showStatus("Disconnected.");
560                 title_prefix = "[Disconnected]";
561                 break;
562
563             // Unknown status code
564             default:
565                 GuacamoleUI.showStatus("[UNKNOWN STATUS]");
566
567         }
568
569         updateTitle();
570     };
571
572     // Name instruction handler
573     guac.onname = function(name) {
574         connection_name = name;
575         updateTitle();
576     };
577
578     // Error handler
579     guac.onerror = function(error) {
580
581         // Disconnect, if connected
582         guac.disconnect();
583
584         // Display error message
585         GuacamoleUI.showError(error);
586         
587     };
588
589     // Disconnect on close
590     window.onunload = function() {
591         guac.disconnect();
592     };
593
594     // Handle clipboard events
595     GuacamoleUI.clipboard.onchange = function() {
596
597         var text = GuacamoleUI.clipboard.value;
598         guac.setClipboard(text);
599
600     };
601
602     // Ignore keypresses when clipboard is focused
603     GuacamoleUI.clipboard.onfocus = function() {
604         disableKeyboard();
605     };
606
607     // Capture keypresses when clipboard is not focused
608     GuacamoleUI.clipboard.onblur = function() {
609         enableKeyboard();
610     };
611
612     // Server copy handler
613     guac.onclipboard = function(data) {
614         GuacamoleUI.clipboard.value = data;
615     };
616
617     GuacamoleUI.keyboard.onkeydown = function(keysym) {
618         guac.sendKeyEvent(1, keysym);
619     };
620
621     GuacamoleUI.keyboard.onkeyup = function(keysym) {
622         guac.sendKeyEvent(0, keysym);
623     };
624
625     // Send Ctrl-Alt-Delete
626     GuacamoleUI.buttons.ctrlAltDelete.onclick = function() {
627
628         var KEYSYM_CTRL   = 0xFFE3;
629         var KEYSYM_ALT    = 0xFFE9;
630         var KEYSYM_DELETE = 0xFFFF;
631
632         guac.sendKeyEvent(1, KEYSYM_CTRL);
633         guac.sendKeyEvent(1, KEYSYM_ALT);
634         guac.sendKeyEvent(1, KEYSYM_DELETE);
635         guac.sendKeyEvent(0, KEYSYM_DELETE);
636         guac.sendKeyEvent(0, KEYSYM_ALT);
637         guac.sendKeyEvent(0, KEYSYM_CTRL);
638     };
639
640 };