Do not require classList.
[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
12     "buttons": {
13
14         "showClipboard": document.getElementById("showClipboard"),
15         "showKeyboard" : document.getElementById("showKeyboard"),
16         "ctrlAltDelete": document.getElementById("ctrlAltDelete"),
17         "reconnect"    : document.getElementById("reconnect"),
18         "logout"       : document.getElementById("logout")
19
20     },
21
22     "containers": {
23         "state"    : document.getElementById("statusDialog"),
24         "clipboard": document.getElementById("clipboardDiv"),
25         "keyboard" : document.getElementById("keyboardContainer")
26     },
27     
28     "state"     : document.getElementById("statusText"),
29     "clipboard" : document.getElementById("clipboard")
30
31 };
32
33 // Constant UI initialization and behavior
34 (function() {
35
36     var menu_shaded = false;
37
38     var shade_interval = null;
39     var show_interval = null;
40
41     // Cache error image (might not be available when error occurs)
42     var guacErrorImage = new Image();
43     guacErrorImage.src = "images/noguacamole-logo-24.png";
44
45     // Function for adding a class to an element
46     var addClass;
47
48     // Function for removing a class from an element
49     var removeClass;
50
51     // If Node.classList is supported, implement addClass/removeClass using that
52     if (Node.classList) {
53
54         addClass = function(element, classname) {
55             element.classList.add(classname);
56         };
57         
58         removeClass = function(element, classname) {
59             element.classList.remove(classname);
60         };
61         
62     }
63
64     // Otherwise, implement own
65     else {
66
67         addClass = function(element, classname) {
68
69             // Simply add new class
70             element.className += " " + classname;
71
72         };
73         
74         removeClass = function(element, classname) {
75
76             // Filter out classes with given name
77             element.className = element.className.replace(/([^ ]+)[ ]*/g,
78                 function(match, testClassname, spaces, offset, string) {
79
80                     // If same class, remove
81                     if (testClassname == classname)
82                         return "";
83
84                     // Otherwise, allow
85                     return match;
86                     
87                 }
88             );
89
90         };
91         
92     }
93
94
95     GuacamoleUI.hideStatus = function() {
96         removeClass(document.body, "guac-error");
97         GuacamoleUI.containers.state.style.visibility = "hidden";
98     };
99     
100     GuacamoleUI.showStatus = function(text) {
101         removeClass(document.body, "guac-error");
102         GuacamoleUI.containers.state.style.visibility = "visible";
103         GuacamoleUI.state.textContent = text;
104     };
105     
106     GuacamoleUI.showError = function(error) {
107         addClass(document.body, "guac-error");
108         GuacamoleUI.state.textContent = error;
109     };
110
111     GuacamoleUI.shadeMenu = function() {
112
113         if (!menu_shaded) {
114
115             var step = Math.floor(GuacamoleUI.menu.offsetHeight / 10) + 1;
116             var offset = 0;
117             menu_shaded = true;
118
119             window.clearInterval(show_interval);
120             shade_interval = window.setInterval(function() {
121
122                 offset -= step;
123                 GuacamoleUI.menu.style.top = offset + "px";
124
125                 if (offset <= -GuacamoleUI.menu.offsetHeight) {
126                     window.clearInterval(shade_interval);
127                     GuacamoleUI.menu.style.visiblity = "hidden";
128                 }
129
130             }, 30);
131         }
132
133     };
134
135     GuacamoleUI.showMenu = function() {
136
137         if (menu_shaded) {
138
139             var step = Math.floor(GuacamoleUI.menu.offsetHeight / 5) + 1;
140             var offset = -GuacamoleUI.menu.offsetHeight;
141             menu_shaded = false;
142             GuacamoleUI.menu.style.visiblity = "";
143
144             window.clearInterval(shade_interval);
145             show_interval = window.setInterval(function() {
146
147                 offset += step;
148
149                 if (offset >= 0) {
150                     offset = 0;
151                     window.clearInterval(show_interval);
152                 }
153
154                 GuacamoleUI.menu.style.top = offset + "px";
155
156             }, 30);
157         }
158
159     };
160
161     // Show/Hide clipboard
162     GuacamoleUI.buttons.showClipboard.onclick = function() {
163
164         var displayed = GuacamoleUI.containers.clipboard.style.display;
165         if (displayed != "block") {
166             GuacamoleUI.containers.clipboard.style.display = "block";
167             GuacamoleUI.buttons.showClipboard.innerHTML = "Hide Clipboard";
168         }
169         else {
170             GuacamoleUI.containers.clipboard.style.display = "none";
171             GuacamoleUI.buttons.showClipboard.innerHTML = "Show Clipboard";
172             GuacamoleUI.clipboard.onchange();
173         }
174
175     };
176
177     // Show/Hide keyboard
178     var keyboardResizeInterval = null;
179     GuacamoleUI.buttons.showKeyboard.onclick = function() {
180
181         var displayed = GuacamoleUI.containers.keyboard.style.display;
182         if (displayed != "block") {
183             GuacamoleUI.containers.keyboard.style.display = "block";
184             GuacamoleUI.buttons.showKeyboard.textContent = "Hide Keyboard";
185
186             // Automatically update size
187             window.onresize = updateKeyboardSize;
188             keyboardResizeInterval = window.setInterval(updateKeyboardSize, 30);
189
190             updateKeyboardSize();
191         }
192         else {
193             GuacamoleUI.containers.keyboard.style.display = "none";
194             GuacamoleUI.buttons.showKeyboard.textContent = "Show Keyboard";
195
196             window.onresize = null;
197             window.clearInterval(keyboardResizeInterval);
198         }
199
200     };
201
202     // Logout
203     GuacamoleUI.buttons.logout.onclick = function() {
204         window.location.href = "logout";
205     };
206
207     // Timeouts for detecting if users wants menu to open or close
208     var detectMenuOpenTimeout = null;
209     var detectMenuCloseTimeout = null;
210
211     // Clear detection timeouts
212     GuacamoleUI.resetMenuDetect = function() {
213
214         if (detectMenuOpenTimeout != null) {
215             window.clearTimeout(detectMenuOpenTimeout);
216             detectMenuOpenTimeout = null;
217         }
218
219         if (detectMenuCloseTimeout != null) {
220             window.clearTimeout(detectMenuCloseTimeout);
221             detectMenuCloseTimeout = null;
222         }
223
224     };
225
226     // Initiate detection of menu open action. If not canceled through some
227     // user event, menu will open.
228     GuacamoleUI.startMenuOpenDetect = function() {
229
230         if (!detectMenuOpenTimeout) {
231
232             // Clear detection state
233             GuacamoleUI.resetMenuDetect();
234
235             // Wait and then show menu
236             detectMenuOpenTimeout = window.setTimeout(function() {
237                 GuacamoleUI.showMenu();
238                 detectMenuOpenTimeout = null;
239             }, 325);
240
241         }
242
243     };
244
245     // Initiate detection of menu close action. If not canceled through some
246     // user event, menu will close.
247     GuacamoleUI.startMenuCloseDetect = function() {
248
249         if (!detectMenuCloseTimeout) {
250
251             // Clear detection state
252             GuacamoleUI.resetMenuDetect();
253
254             // Wait and then shade menu
255             detectMenuCloseTimeout = window.setTimeout(function() {
256                 GuacamoleUI.shadeMenu();
257                 detectMenuCloseTimeout = null;
258             }, 500);
259
260         }
261
262     };
263
264     // Show menu if mouseover any part of menu
265     GuacamoleUI.menu.addEventListener('mouseover', GuacamoleUI.showMenu, true);
266
267     // Stop detecting menu state change intents if mouse is over menu
268     GuacamoleUI.menu.addEventListener('mouseover', GuacamoleUI.resetMenuDetect, true);
269
270     // When mouse hovers over top of screen, start detection of intent to open menu
271     GuacamoleUI.menuControl.addEventListener('mousemove', GuacamoleUI.startMenuOpenDetect, true);
272
273     var menuShowLongPressTimeout = null;
274
275     GuacamoleUI.startLongPressDetect = function() {
276
277         if (!menuShowLongPressTimeout) {
278
279             menuShowLongPressTimeout = window.setTimeout(function() {
280                 
281                 menuShowLongPressTimeout = null;
282                 GuacamoleUI.showMenu();
283
284             }, 800);
285
286         }
287     };
288
289     GuacamoleUI.stopLongPressDetect = function() {
290         window.clearTimeout(menuShowLongPressTimeout);
291         menuShowLongPressTimeout = null;
292     };
293
294     // Detect long-press at bottom of screen
295     document.body.addEventListener('touchstart', GuacamoleUI.startLongPressDetect, true);
296
297     // Reconnect button
298     GuacamoleUI.buttons.reconnect.onclick = function() {
299         window.location.reload();
300     };
301
302     // On-screen keyboard
303     GuacamoleUI.keyboard = new Guacamole.OnScreenKeyboard("layouts/en-us-qwerty-mobile.xml");
304     GuacamoleUI.containers.keyboard.appendChild(GuacamoleUI.keyboard.getElement());
305
306     // Function for automatically updating keyboard size
307     var lastKeyboardWidth;
308     function updateKeyboardSize() {
309         var currentSize = GuacamoleUI.keyboard.getElement().offsetWidth;
310         if (lastKeyboardWidth != currentSize) {
311             GuacamoleUI.keyboard.resize(currentSize);
312             lastKeyboardWidth = currentSize;
313         }
314     };
315
316 })();
317
318 // Tie UI events / behavior to a specific Guacamole client
319 GuacamoleUI.attach = function(guac) {
320
321     var guac_display = guac.getDisplay();
322
323     // When mouse enters display, start detection of intent to close menu
324     guac_display.addEventListener('mouseover', GuacamoleUI.startMenuCloseDetect, true);
325
326     guac_display.onclick = function(e) {
327         e.preventDefault();
328         return false;
329     };
330
331     // Mouse
332     var mouse = new Guacamole.Mouse(guac_display);
333     mouse.onmousedown = mouse.onmouseup = mouse.onmousemove =
334         function(mouseState) {
335        
336             // Determine mouse position within view
337             var mouse_view_x = mouseState.x + guac_display.offsetLeft - window.pageXOffset;
338             var mouse_view_y = mouseState.y + guac_display.offsetTop  - window.pageYOffset;
339
340             // Determine viewport dimensioins
341             var view_width  = GuacamoleUI.viewport.offsetWidth;
342             var view_height = GuacamoleUI.viewport.offsetHeight;
343
344             // Determine scroll amounts based on mouse position relative to document
345
346             var scroll_amount_x;
347             if (mouse_view_x > view_width)
348                 scroll_amount_x = mouse_view_x - view_width;
349             else if (mouse_view_x < 0)
350                 scroll_amount_x = mouse_view_x;
351             else
352                 scroll_amount_x = 0;
353
354             var scroll_amount_y;
355             if (mouse_view_y > view_height)
356                 scroll_amount_y = mouse_view_y - view_height;
357             else if (mouse_view_y < 0)
358                 scroll_amount_y = mouse_view_y;
359             else
360                 scroll_amount_y = 0;
361
362             // Scroll (if necessary) to keep mouse on screen.
363             window.scrollBy(scroll_amount_x, scroll_amount_y);
364        
365             // Hide menu on movement
366             GuacamoleUI.startMenuCloseDetect();
367
368             // Stop detecting long presses if mouse is being used
369             GuacamoleUI.stopLongPressDetect();
370
371             // Send mouse event
372             guac.sendMouseState(mouseState);
373             
374         };
375
376     // Keyboard
377     var keyboard = new Guacamole.Keyboard(document);
378
379     function disableKeyboard() {
380         keyboard.onkeydown = null;
381         keyboard.onkeyup = null;
382     }
383
384     function enableKeyboard() {
385         keyboard.onkeydown = 
386             function (keysym) {
387                 guac.sendKeyEvent(1, keysym);
388             };
389
390         keyboard.onkeyup = 
391             function (keysym) {
392                 guac.sendKeyEvent(0, keysym);
393             };
394     }
395
396     // Enable keyboard by default
397     enableKeyboard();
398
399     // Handle client state change
400     guac.onstatechange = function(clientState) {
401         switch (clientState) {
402
403             // Idle
404             case 0:
405                 GuacamoleUI.showStatus("Idle.");
406                 break;
407
408             // Connecting
409             case 1:
410                 GuacamoleUI.shadeMenu();
411                 GuacamoleUI.showStatus("Connecting...");
412                 break;
413
414             // Connected + waiting
415             case 2:
416                 GuacamoleUI.showStatus("Connected, waiting for first update...");
417                 break;
418
419             // Connected
420             case 3:
421                 
422                 GuacamoleUI.hideStatus();
423                 GuacamoleUI.display.className =
424                     GuacamoleUI.display.className.replace(/guac-loading/, '');
425
426                 GuacamoleUI.menu.className = "connected";
427                 break;
428
429             // Disconnecting
430             case 4:
431                 GuacamoleUI.showStatus("Disconnecting...");
432                 break;
433
434             // Disconnected
435             case 5:
436                 GuacamoleUI.showStatus("Disconnected.");
437                 break;
438
439             // Unknown status code
440             default:
441                 GuacamoleUI.showStatus("[UNKNOWN STATUS]");
442
443         }
444     };
445
446     // Name instruction handler
447     guac.onname = function(name) {
448         document.title = name;
449     };
450
451     // Error handler
452     guac.onerror = function(error) {
453
454         // Disconnect, if connected
455         guac.disconnect();
456
457         // Display error message
458         GuacamoleUI.showError(error);
459         
460     };
461
462     // Disconnect on close
463     window.onunload = function() {
464         guac.disconnect();
465     };
466
467     // Handle clipboard events
468     GuacamoleUI.clipboard.onchange = function() {
469
470         var text = GuacamoleUI.clipboard.value;
471         guac.setClipboard(text);
472
473     };
474
475     // Ignore keypresses when clipboard is focused
476     GuacamoleUI.clipboard.onfocus = function() {
477         disableKeyboard();
478     };
479
480     // Capture keypresses when clipboard is not focused
481     GuacamoleUI.clipboard.onblur = function() {
482         enableKeyboard();
483     };
484
485     // Server copy handler
486     guac.onclipboard = function(data) {
487         GuacamoleUI.clipboard.value = data;
488     };
489
490     GuacamoleUI.keyboard.onkeydown = function(keysym) {
491         guac.sendKeyEvent(1, keysym);
492     };
493
494     GuacamoleUI.keyboard.onkeyup = function(keysym) {
495         guac.sendKeyEvent(0, keysym);
496     };
497
498     // Send Ctrl-Alt-Delete
499     GuacamoleUI.buttons.ctrlAltDelete.onclick = function() {
500
501         var KEYSYM_CTRL   = 0xFFE3;
502         var KEYSYM_ALT    = 0xFFE9;
503         var KEYSYM_DELETE = 0xFFFF;
504
505         guac.sendKeyEvent(1, KEYSYM_CTRL);
506         guac.sendKeyEvent(1, KEYSYM_ALT);
507         guac.sendKeyEvent(1, KEYSYM_DELETE);
508         guac.sendKeyEvent(0, KEYSYM_DELETE);
509         guac.sendKeyEvent(0, KEYSYM_ALT);
510         guac.sendKeyEvent(0, KEYSYM_CTRL);
511     };
512
513 };