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