dfa1b3d667fad020c683ef53bd5f1d29c03054e5
[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     // Assume no native OSK by default
182     GuacamoleUI.assumeNativeOSK = false;
183
184     // Show/Hide keyboard
185     var keyboardResizeInterval = null;
186     GuacamoleUI.buttons.showKeyboard.onclick = function() {
187
188         // If we think the platform has a native OSK, use the event target to
189         // cause it to display.
190         if (GuacamoleUI.assumeNativeOSK) {
191             GuacamoleUI.eventTarget.focus();
192             return;
193         }
194         
195         var displayed = GuacamoleUI.containers.keyboard.style.display;
196         if (displayed != "block") {
197             GuacamoleUI.containers.keyboard.style.display = "block";
198             GuacamoleUI.buttons.showKeyboard.textContent = "Hide Keyboard";
199
200             // Automatically update size
201             window.onresize = updateKeyboardSize;
202             keyboardResizeInterval = window.setInterval(updateKeyboardSize, 30);
203
204             updateKeyboardSize();
205         }
206         else {
207             GuacamoleUI.containers.keyboard.style.display = "none";
208             GuacamoleUI.buttons.showKeyboard.textContent = "Show Keyboard";
209
210             window.onresize = null;
211             window.clearInterval(keyboardResizeInterval);
212         }
213
214     };
215
216     // Logout
217     GuacamoleUI.buttons.logout.onclick = function() {
218         window.location.href = "logout";
219     };
220
221     // Timeouts for detecting if users wants menu to open or close
222     var detectMenuOpenTimeout = null;
223     var detectMenuCloseTimeout = null;
224
225     // Clear detection timeouts
226     GuacamoleUI.resetMenuDetect = function() {
227
228         if (detectMenuOpenTimeout != null) {
229             window.clearTimeout(detectMenuOpenTimeout);
230             detectMenuOpenTimeout = null;
231         }
232
233         if (detectMenuCloseTimeout != null) {
234             window.clearTimeout(detectMenuCloseTimeout);
235             detectMenuCloseTimeout = null;
236         }
237
238     };
239
240     // Initiate detection of menu open action. If not canceled through some
241     // user event, menu will open.
242     GuacamoleUI.startMenuOpenDetect = function() {
243
244         if (!detectMenuOpenTimeout) {
245
246             // Clear detection state
247             GuacamoleUI.resetMenuDetect();
248
249             // Wait and then show menu
250             detectMenuOpenTimeout = window.setTimeout(function() {
251                 GuacamoleUI.showMenu();
252                 detectMenuOpenTimeout = null;
253             }, 325);
254
255         }
256
257     };
258
259     // Initiate detection of menu close action. If not canceled through some
260     // user event, menu will close.
261     GuacamoleUI.startMenuCloseDetect = function() {
262
263         if (!detectMenuCloseTimeout) {
264
265             // Clear detection state
266             GuacamoleUI.resetMenuDetect();
267
268             // Wait and then shade menu
269             detectMenuCloseTimeout = window.setTimeout(function() {
270                 GuacamoleUI.shadeMenu();
271                 detectMenuCloseTimeout = null;
272             }, 500);
273
274         }
275
276     };
277
278     // Show menu if mouseover any part of menu
279     GuacamoleUI.menu.addEventListener('mouseover', GuacamoleUI.showMenu, true);
280
281     // Stop detecting menu state change intents if mouse is over menu
282     GuacamoleUI.menu.addEventListener('mouseover', GuacamoleUI.resetMenuDetect, true);
283
284     // When mouse hovers over top of screen, start detection of intent to open menu
285     GuacamoleUI.menuControl.addEventListener('mousemove', GuacamoleUI.startMenuOpenDetect, true);
286
287     var menuShowLongPressTimeout = null;
288
289     GuacamoleUI.startLongPressDetect = function() {
290
291         if (!menuShowLongPressTimeout) {
292
293             menuShowLongPressTimeout = window.setTimeout(function() {
294                 
295                 menuShowLongPressTimeout = null;
296
297                 // Assume native OSK if menu shown via long-press
298                 GuacamoleUI.assumeNativeOSK = true;
299                 GuacamoleUI.showMenu();
300
301             }, 800);
302
303         }
304     };
305
306     GuacamoleUI.stopLongPressDetect = function() {
307         window.clearTimeout(menuShowLongPressTimeout);
308         menuShowLongPressTimeout = null;
309     };
310
311     // Reset event target (add content, reposition cursor in middle.
312     GuacamoleUI.resetEventTarget = function() {
313         GuacamoleUI.eventTarget.value = "GUAC";
314         GuacamoleUI.eventTarget.selectionStart =
315         GuacamoleUI.eventTarget.selectionEnd   = 2;
316     };
317
318     // Detect long-press at bottom of screen
319     document.body.addEventListener('touchstart', GuacamoleUI.startLongPressDetect, true);
320
321     // Reconnect button
322     GuacamoleUI.buttons.reconnect.onclick = function() {
323         window.location.reload();
324     };
325
326     // On-screen keyboard
327     GuacamoleUI.keyboard = new Guacamole.OnScreenKeyboard("layouts/en-us-qwerty-mobile.xml");
328     GuacamoleUI.containers.keyboard.appendChild(GuacamoleUI.keyboard.getElement());
329
330     // Function for automatically updating keyboard size
331     var lastKeyboardWidth;
332     function updateKeyboardSize() {
333         var currentSize = GuacamoleUI.keyboard.getElement().offsetWidth;
334         if (lastKeyboardWidth != currentSize) {
335             GuacamoleUI.keyboard.resize(currentSize);
336             lastKeyboardWidth = currentSize;
337         }
338     };
339
340 })();
341
342 // Tie UI events / behavior to a specific Guacamole client
343 GuacamoleUI.attach = function(guac) {
344
345     var guac_display = guac.getDisplay();
346
347     // When mouse enters display, start detection of intent to close menu
348     guac_display.addEventListener('mouseover', GuacamoleUI.startMenuCloseDetect, true);
349
350     guac_display.onclick = function(e) {
351         e.preventDefault();
352         return false;
353     };
354
355     // Mouse
356     var mouse = new Guacamole.Mouse(guac_display);
357     mouse.onmousedown = mouse.onmouseup = mouse.onmousemove =
358         function(mouseState) {
359        
360             // Determine mouse position within view
361             var mouse_view_x = mouseState.x + guac_display.offsetLeft - window.pageXOffset;
362             var mouse_view_y = mouseState.y + guac_display.offsetTop  - window.pageYOffset;
363
364             // Determine viewport dimensioins
365             var view_width  = GuacamoleUI.viewport.offsetWidth;
366             var view_height = GuacamoleUI.viewport.offsetHeight;
367
368             // Determine scroll amounts based on mouse position relative to document
369
370             var scroll_amount_x;
371             if (mouse_view_x > view_width)
372                 scroll_amount_x = mouse_view_x - view_width;
373             else if (mouse_view_x < 0)
374                 scroll_amount_x = mouse_view_x;
375             else
376                 scroll_amount_x = 0;
377
378             var scroll_amount_y;
379             if (mouse_view_y > view_height)
380                 scroll_amount_y = mouse_view_y - view_height;
381             else if (mouse_view_y < 0)
382                 scroll_amount_y = mouse_view_y;
383             else
384                 scroll_amount_y = 0;
385
386             // Scroll (if necessary) to keep mouse on screen.
387             window.scrollBy(scroll_amount_x, scroll_amount_y);
388        
389             // Hide menu on movement
390             GuacamoleUI.startMenuCloseDetect();
391
392             // Stop detecting long presses if mouse is being used
393             GuacamoleUI.stopLongPressDetect();
394
395             // Send mouse event
396             guac.sendMouseState(mouseState);
397             
398         };
399
400     // Keyboard
401     var keyboard = new Guacamole.Keyboard(document);
402
403     function disableKeyboard() {
404         keyboard.onkeydown = null;
405         keyboard.onkeyup = null;
406     }
407
408     function enableKeyboard() {
409         keyboard.onkeydown = 
410             function (keysym) {
411           
412                 // If we're using native OSK, ensure event target is reset
413                 // on each key event.
414                 if (GuacamoleUI.assumeNativeOSK)
415                     GuacamoleUI.resetEventTarget();
416                 
417                 guac.sendKeyEvent(1, keysym);
418             };
419
420         keyboard.onkeyup = 
421             function (keysym) {
422                 guac.sendKeyEvent(0, keysym);
423             };
424     }
425
426     // Enable keyboard by default
427     enableKeyboard();
428
429     // Handle client state change
430     guac.onstatechange = function(clientState) {
431         switch (clientState) {
432
433             // Idle
434             case 0:
435                 GuacamoleUI.showStatus("Idle.");
436                 break;
437
438             // Connecting
439             case 1:
440                 GuacamoleUI.shadeMenu();
441                 GuacamoleUI.showStatus("Connecting...");
442                 break;
443
444             // Connected + waiting
445             case 2:
446                 GuacamoleUI.showStatus("Connected, waiting for first update...");
447                 break;
448
449             // Connected
450             case 3:
451                 
452                 GuacamoleUI.hideStatus();
453                 GuacamoleUI.display.className =
454                     GuacamoleUI.display.className.replace(/guac-loading/, '');
455
456                 GuacamoleUI.menu.className = "connected";
457                 break;
458
459             // Disconnecting
460             case 4:
461                 GuacamoleUI.showStatus("Disconnecting...");
462                 break;
463
464             // Disconnected
465             case 5:
466                 GuacamoleUI.showStatus("Disconnected.");
467                 break;
468
469             // Unknown status code
470             default:
471                 GuacamoleUI.showStatus("[UNKNOWN STATUS]");
472
473         }
474     };
475
476     // Name instruction handler
477     guac.onname = function(name) {
478         document.title = name;
479     };
480
481     // Error handler
482     guac.onerror = function(error) {
483
484         // Disconnect, if connected
485         guac.disconnect();
486
487         // Display error message
488         GuacamoleUI.showError(error);
489         
490     };
491
492     // Disconnect on close
493     window.onunload = function() {
494         guac.disconnect();
495     };
496
497     // Handle clipboard events
498     GuacamoleUI.clipboard.onchange = function() {
499
500         var text = GuacamoleUI.clipboard.value;
501         guac.setClipboard(text);
502
503     };
504
505     // Ignore keypresses when clipboard is focused
506     GuacamoleUI.clipboard.onfocus = function() {
507         disableKeyboard();
508     };
509
510     // Capture keypresses when clipboard is not focused
511     GuacamoleUI.clipboard.onblur = function() {
512         enableKeyboard();
513     };
514
515     // Server copy handler
516     guac.onclipboard = function(data) {
517         GuacamoleUI.clipboard.value = data;
518     };
519
520     GuacamoleUI.keyboard.onkeydown = function(keysym) {
521         guac.sendKeyEvent(1, keysym);
522     };
523
524     GuacamoleUI.keyboard.onkeyup = function(keysym) {
525         guac.sendKeyEvent(0, keysym);
526     };
527
528     // Send Ctrl-Alt-Delete
529     GuacamoleUI.buttons.ctrlAltDelete.onclick = function() {
530
531         var KEYSYM_CTRL   = 0xFFE3;
532         var KEYSYM_ALT    = 0xFFE9;
533         var KEYSYM_DELETE = 0xFFFF;
534
535         guac.sendKeyEvent(1, KEYSYM_CTRL);
536         guac.sendKeyEvent(1, KEYSYM_ALT);
537         guac.sendKeyEvent(1, KEYSYM_DELETE);
538         guac.sendKeyEvent(0, KEYSYM_DELETE);
539         guac.sendKeyEvent(0, KEYSYM_ALT);
540         guac.sendKeyEvent(0, KEYSYM_CTRL);
541     };
542
543 };