| /* |
| * Copyright (C) 2013 Glyptodon LLC |
| * |
| * Permission is hereby granted, free of charge, to any person obtaining a copy |
| * of this software and associated documentation files (the "Software"), to deal |
| * in the Software without restriction, including without limitation the rights |
| * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| * copies of the Software, and to permit persons to whom the Software is |
| * furnished to do so, subject to the following conditions: |
| * |
| * The above copyright notice and this permission notice shall be included in |
| * all copies or substantial portions of the Software. |
| * |
| * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| * THE SOFTWARE. |
| */ |
| |
| /** |
| * Client UI root object. |
| */ |
| GuacUI.Client = { |
| |
| /** |
| * Enumeration of all tunnel-specific error messages for each applicable |
| * error code. |
| */ |
| "tunnel_errors": { |
| |
| 0x0201: "The Guacamole server has rejected this connection attempt \ |
| because there are too many active connections. Please wait \ |
| a few minutes and try again.", |
| |
| 0x0202: "The connection has been closed because the server is taking \ |
| too long to respond. This is usually caused by network \ |
| problems, such as a spotty wireless signal, or slow network \ |
| speeds. Please check your network connection and try again \ |
| or contact your system administrator.", |
| |
| 0x0203: "The server encountered an error and has closed the \ |
| connection. Please try again or contact your \ |
| system administrator.", |
| |
| 0x0204: "The requested connection does not exist. Please check the \ |
| connection name and try again.", |
| |
| 0x0205: "This connection is currently in use, and concurrent access to \ |
| this connection is not allowed. Please try again later.", |
| |
| 0x0301: "You do not have permission to access this connection because \ |
| you are not logged in. Please log in and try again.", |
| |
| 0x0303: "You do not have permission to access this connection. If you \ |
| require access, please ask your system administrator to add \ |
| you the list of allowed users, or check your system settings.", |
| |
| 0x0308: "The Guacamole server has closed the connection because there \ |
| has been no response from your browser for long enough that \ |
| it appeared to be disconnected. This is commonly caused by \ |
| network problems, such as spotty wireless signal, or simply \ |
| very slow network speeds. Please check your network and try \ |
| again.", |
| |
| 0x031D: "The Guacamole server is denying access to this connection \ |
| because you have exhausted the limit for simultaneous \ |
| connection use by an individual user. Please close one or \ |
| more connections and try again.", |
| |
| "DEFAULT": "An internal error has occurred within the Guacamole \ |
| server, and the connection has been terminated. If \ |
| the problem persists, please notify your system \ |
| administrator, or check your system logs." |
| |
| }, |
| |
| /** |
| * Enumeration of all client-specific error messages for each applicable |
| * error code. |
| */ |
| "client_errors": { |
| |
| 0x0201: "This connection has been closed because the server is busy. \ |
| Please wait a few minutes and try again.", |
| |
| 0x0202: "The Guacamole server has closed the connection because the \ |
| remote desktop is taking too long to respond. Please try \ |
| again or contact your system administrator.", |
| |
| 0x0203: "The remote desktop server encountered an error and has closed \ |
| the connection. Please try again or contact your system \ |
| administrator.", |
| |
| 0x0205: "This connection has been closed because it conflicts with \ |
| another connection. Please try again later.", |
| |
| 0x0301: "Log in failed. Please reconnect and try again.", |
| |
| 0x0303: "You do not have permission to access this connection. If you \ |
| require access, please ask your system administrator to add \ |
| you the list of allowed users, or check your system settings.", |
| |
| 0x0308: "The Guacamole server has closed the connection because there \ |
| has been no response from your browser for long enough that \ |
| it appeared to be disconnected. This is commonly caused by \ |
| network problems, such as spotty wireless signal, or simply \ |
| very slow network speeds. Please check your network and try \ |
| again.", |
| |
| 0x031D: "The Guacamole server is denying access to this connection \ |
| because you have exhausted the limit for simultaneous \ |
| connection use by an individual user. Please close one or \ |
| more connections and try again.", |
| |
| "DEFAULT": "An internal error has occurred within the Guacamole \ |
| server, and the connection has been terminated. If \ |
| the problem persists, please notify your system \ |
| administrator, or check your system logs." |
| |
| }, |
| |
| /** |
| * Enumeration of all error messages for each applicable error code. This |
| * list is specific to file uploads. |
| */ |
| "upload_errors": { |
| |
| 0x0100: "File transfer is either not supported or not enabled. Please \ |
| contact your system administrator, or check your system logs.", |
| |
| 0x0201: "Too many files are currently being transferred. Please wait \ |
| for existing transfers to complete, and then try again.", |
| |
| 0x0202: "The file cannot be transferred because the remote desktop \ |
| server is taking too long to respond. Please try again or \ |
| or contact your system administrator.", |
| |
| 0x0203: "The remote desktop server encountered an error during \ |
| transfer. Please try again or contact your system \ |
| administrator.", |
| |
| 0x0204: "The destination for the file transfer does not exist. Please \ |
| check that the destionation exists and try again.", |
| |
| 0x0205: "The destination for the file transfer is currently locked. \ |
| Please wait for any in-progress tasks to complete and try \ |
| again.", |
| |
| 0x0301: "You do not have permission to upload this file because you \ |
| are not logged in. Please log in and try again.", |
| |
| 0x0303: "You do not have permission to upload this file. If you \ |
| require access, please check your system settings, or \ |
| check with your system administrator.", |
| |
| 0x0308: "The file transfer has stalled. This is commonly caused by \ |
| network problems, such as spotty wireless signal, or \ |
| simply very slow network speeds. Please check your \ |
| network and try again.", |
| |
| 0x031D: "Too many files are currently being transferred. Please wait \ |
| for existing transfers to complete, and then try again.", |
| |
| "DEFAULT": "An internal error has occurred within the Guacamole \ |
| server, and the connection has been terminated. If \ |
| the problem persists, please notify your system \ |
| administrator, or check your system logs.", |
| |
| }, |
| |
| /** |
| * All error codes for which automatic reconnection is appropriate when a |
| * tunnel error occurs. |
| */ |
| "tunnel_auto_reconnect": { |
| 0x0200: true, |
| 0x0202: true, |
| 0x0203: true, |
| 0x0308: true |
| }, |
| |
| /** |
| * All error codes for which automatic reconnection is appropriate when a |
| * client error occurs. |
| */ |
| "client_auto_reconnect": { |
| 0x0200: true, |
| 0x0202: true, |
| 0x0203: true, |
| 0x0301: true, |
| 0x0308: true |
| }, |
| |
| /* Constants */ |
| |
| "KEYBOARD_AUTO_RESIZE_INTERVAL" : 30, /* milliseconds */ |
| "RECONNECT_PERIOD" : 15, /* seconds */ |
| "TEXT_INPUT_PADDING" : 128, /* characters */ |
| "TEXT_INPUT_PADDING_CODEPOINT" : 0x200B, |
| |
| /* Main application area */ |
| |
| "viewport" : document.getElementById("viewportClone"), |
| "main" : document.getElementById("main"), |
| "display" : document.getElementById("display"), |
| "notification_area" : document.getElementById("notificationArea"), |
| |
| /* Text input */ |
| |
| "text_input" : { |
| "container" : document.getElementById("text-input"), |
| "sent" : document.getElementById("sent-history"), |
| "target" : document.getElementById("target"), |
| "enabled" : false |
| }, |
| |
| /* Menu */ |
| |
| "menu" : document.getElementById("menu"), |
| "menu_title" : document.getElementById("menu-title"), |
| "clipboard" : document.getElementById("clipboard"), |
| "relative_radio" : document.getElementById("relative"), |
| "absolute_radio" : document.getElementById("absolute"), |
| "ime_none_radio" : document.getElementById("ime-none"), |
| "ime_text_radio" : document.getElementById("ime-text"), |
| "ime_osk_radio" : document.getElementById("ime-osk"), |
| "zoom_state" : document.getElementById("zoom-state"), |
| "zoom_out" : document.getElementById("zoom-out"), |
| "zoom_in" : document.getElementById("zoom-in"), |
| "auto_fit" : document.getElementById("auto-fit"), |
| |
| "min_zoom" : 1, |
| "max_zoom" : 3, |
| |
| "connectionName" : "Guacamole", |
| "attachedClient" : null, |
| |
| /* Mouse emulation */ |
| |
| "emulate_absolute" : true, |
| "touch" : null, |
| "touch_screen" : null, |
| "touch_pad" : null, |
| |
| /* Clipboard */ |
| |
| "remote_clipboard" : "", |
| "clipboard_integration_enabled" : undefined |
| |
| }; |
| |
| /** |
| * On-screen Keyboard. This component provides a clickable/touchable keyboard |
| * which sends key events to the Guacamole client. |
| * |
| * @constructor |
| */ |
| GuacUI.Client.OnScreenKeyboard = new (function() { |
| |
| /** |
| * Event target. This is a hidden textarea element which will receive |
| * key events. |
| * @private |
| */ |
| var keyboard_container = GuacUI.createElement("div", "keyboard-container"); |
| |
| var keyboard_resize_interval = null; |
| |
| // On-screen keyboard |
| var keyboard = new Guacamole.OnScreenKeyboard("layouts/en-us-qwerty.xml"); |
| keyboard_container.appendChild(keyboard.getElement()); |
| |
| var last_keyboard_width = 0; |
| |
| // Function for automatically updating keyboard size |
| function updateKeyboardSize() { |
| var currentSize = keyboard.getElement().offsetWidth; |
| if (last_keyboard_width != currentSize) { |
| keyboard.resize(currentSize); |
| last_keyboard_width = currentSize; |
| } |
| } |
| |
| keyboard.onkeydown = function(keysym) { |
| if (GuacUI.Client.attachedClient) |
| GuacUI.Client.attachedClient.sendKeyEvent(1, keysym); |
| }; |
| |
| keyboard.onkeyup = function(keysym) { |
| if (GuacUI.Client.attachedClient) |
| GuacUI.Client.attachedClient.sendKeyEvent(0, keysym); |
| }; |
| |
| this.show = function() { |
| |
| // Only add if not already present |
| if (keyboard_container.parentNode === document.body) |
| return; |
| |
| // Show keyboard |
| document.body.appendChild(keyboard_container); |
| |
| // Start periodic update of keyboard size |
| keyboard_resize_interval = window.setInterval( |
| updateKeyboardSize, |
| GuacUI.Client.KEYBOARD_AUTO_RESIZE_INTERVAL); |
| |
| // Resize on window resize |
| window.addEventListener("resize", updateKeyboardSize, true); |
| |
| // Initialize size |
| updateKeyboardSize(); |
| |
| }; |
| |
| this.hide = function() { |
| |
| // Only remove if present |
| if (keyboard_container.parentNode !== document.body) |
| return; |
| |
| // Hide keyboard |
| document.body.removeChild(keyboard_container); |
| window.clearInterval(keyboard_resize_interval); |
| window.removeEventListener("resize", updateKeyboardSize, true); |
| |
| }; |
| |
| })(); |
| |
| /** |
| * Modal status display. Displays a message to the user, covering the entire |
| * screen. |
| * |
| * Normally, this should only be used when user interaction with other |
| * components is impossible. |
| * |
| * @constructor |
| */ |
| GuacUI.Client.ModalStatus = function(title_text, text, classname, reconnect) { |
| |
| // Create element hierarchy |
| var outer = GuacUI.createElement("div", "dialogOuter"); |
| var middle = GuacUI.createChildElement(outer, "div", "dialogMiddle"); |
| var dialog = GuacUI.createChildElement(middle, "div", "dialog"); |
| |
| // Add title if given |
| if (title_text) { |
| var title = GuacUI.createChildElement(dialog, "p", "title"); |
| title.textContent = title_text; |
| } |
| |
| var status = GuacUI.createChildElement(dialog, "p", "status"); |
| status.textContent = text; |
| |
| // Set classname if given |
| if (classname) |
| GuacUI.addClass(outer, classname); |
| |
| // Automatically reconnect after the given time period |
| var reconnect_interval = null; |
| var reconnect_forced = false; |
| |
| /** |
| * Stops the reconnect countdown and forces a client reconnect. |
| */ |
| function force_reconnect() { |
| if (!reconnect_forced) { |
| reconnect_forced = true; |
| window.clearInterval(reconnect_interval); |
| GuacUI.Client.connect(); |
| } |
| } |
| |
| if (reconnect) { |
| |
| var countdown = GuacUI.createChildElement(dialog, "p", "countdown"); |
| |
| function update_status() { |
| |
| // Use appropriate description of time remaining |
| if (reconnect === 0) |
| countdown.textContent = "Reconnecting..."; |
| if (reconnect === 1) |
| countdown.textContent = "Reconnecting in 1 second..."; |
| else |
| countdown.textContent = "Reconnecting in " + reconnect + " seconds..."; |
| |
| // Reconnect if countdown complete |
| if (reconnect === 0) |
| force_reconnect(); |
| |
| } |
| |
| // Update counter every second |
| reconnect_interval = window.setInterval(function update_countdown() { |
| reconnect--; |
| update_status(); |
| }, 1000); |
| |
| // Init status |
| update_status(); |
| |
| } |
| |
| // Reconnect button |
| var reconnect_section = GuacUI.createChildElement(dialog, "div", "reconnect"); |
| var reconnect_button = GuacUI.createChildElement(reconnect_section, "button"); |
| reconnect_button.textContent = "Reconnect"; |
| |
| // Reconnect if button clicked |
| reconnect_button.onclick = force_reconnect; |
| |
| // Reconnect if button tapped |
| reconnect_button.addEventListener("touchend", function(e) { |
| if (e.touches.length === 0) |
| force_reconnect(); |
| }, true); |
| |
| this.show = function() { |
| document.body.appendChild(outer); |
| }; |
| |
| this.hide = function() { |
| window.clearInterval(reconnect_interval); |
| document.body.removeChild(outer); |
| }; |
| |
| }; |
| |
| /** |
| * Monitors a given element for touch events, firing drag-specific events |
| * based on pre-defined gestures. |
| * |
| * @constructor |
| * @param {Element} element The element to monitor for touch events. |
| */ |
| GuacUI.Client.Drag = function(element) { |
| |
| /** |
| * Reference to this drag instance. |
| * @private |
| */ |
| var guac_drag = this; |
| |
| /** |
| * Whether a drag gestures is in progress. |
| */ |
| var in_progress = false; |
| |
| /** |
| * The starting X location of the drag gesture. |
| */ |
| this.start_x = null; |
| |
| /** |
| * The starting Y location of the drag gesture. |
| */ |
| this.start_y = null; |
| |
| /** |
| * The change in X relative to drag start. |
| */ |
| this.delta_x = 0; |
| |
| /** |
| * The change in X relative to drag start. |
| */ |
| this.delta_y = 0; |
| |
| /** |
| * Called when a drag gesture begins. |
| * |
| * @event |
| * @param {Number} x The relative change in X location relative to |
| * drag start. For drag start, this will ALWAYS be 0. |
| * @param {Number} y The relative change in Y location relative to |
| * drag start. For drag start, this will ALWAYS be 0. |
| */ |
| this.ondragstart = null; |
| |
| /** |
| * Called when the drag amount changes. |
| * |
| * @event |
| * @param {Number} x The relative change in X location relative to |
| * drag start. |
| * @param {Number} y The relative change in Y location relative to |
| * drag start. |
| */ |
| this.ondragchange = null; |
| |
| /** |
| * Called when a drag gesture ends. |
| * |
| * @event |
| * @param {Number} x The relative change in X location relative to |
| * drag start. |
| * @param {Number} y The relative change in Y location relative to |
| * drag start. |
| */ |
| this.ondragend = null; |
| |
| /** |
| * Cancels the current drag gesture, if any. Drag events will cease to fire |
| * until a new gesture begins. |
| */ |
| this.cancel = function() { |
| in_progress = false; |
| }; |
| |
| // When there is exactly one touch, monitor the change in location |
| element.addEventListener("touchmove", function(e) { |
| if (e.touches.length === 1) { |
| |
| e.preventDefault(); |
| e.stopPropagation(); |
| |
| // Get touch location |
| var x = e.touches[0].clientX; |
| var y = e.touches[0].clientY; |
| |
| // If gesture just starting, fire zoom start |
| if (!guac_drag.start_x || !guac_drag.start_y) { |
| guac_drag.start_x = x; |
| guac_drag.start_y = y; |
| guac_drag.delta_x = 0; |
| guac_drag.delta_y = 0; |
| in_progress = true; |
| if (guac_drag.ondragstart) |
| guac_drag.ondragstart(guac_drag.delta_x, guac_drag.delta_y); |
| } |
| |
| // Otherwise, notify of zoom change |
| else if (guac_drag.ondragchange) { |
| guac_drag.delta_x = x - guac_drag.start_x; |
| guac_drag.delta_y = y - guac_drag.start_y; |
| |
| if (in_progress) |
| guac_drag.ondragchange(guac_drag.delta_x, guac_drag.delta_y); |
| } |
| |
| } |
| }, false); |
| |
| // Reset monitoring and fire end event when done |
| element.addEventListener("touchend", function(e) { |
| |
| if (guac_drag.start_x && guac_drag.start_y && e.touches.length === 0) { |
| |
| e.preventDefault(); |
| e.stopPropagation(); |
| |
| if (in_progress && guac_drag.ondragend) |
| guac_drag.ondragend(); |
| |
| guac_drag.start_x = null; |
| guac_drag.start_y = null; |
| guac_drag.delta_x = 0; |
| guac_drag.delta_y = 0; |
| in_progress = false; |
| |
| } |
| |
| }, false); |
| |
| }; |
| |
| /** |
| * Monitors a given element for touch events, firing zoom-specific events |
| * based on pre-defined gestures. |
| * |
| * @constructor |
| * @param {Element} element The element to monitor for touch events. |
| */ |
| GuacUI.Client.Pinch = function(element) { |
| |
| /** |
| * Reference to this zoom instance. |
| * @private |
| */ |
| var guac_zoom = this; |
| |
| /** |
| * The current pinch distance, or null if the gesture has not yet started. |
| * @private |
| */ |
| var start_length = null; |
| |
| /** |
| * The current zoom ratio. |
| * @type Number |
| */ |
| this.ratio = 1; |
| |
| /** |
| * The X-coordinate of the current center of the pinch gesture. |
| * @type Number |
| */ |
| this.centerX = 0; |
| |
| /** |
| * The Y-coordinate of the current center of the pinch gesture. |
| * @type Number |
| */ |
| this.centerY = 0; |
| |
| /** |
| * Called when a zoom gesture begins. |
| * |
| * @event |
| * @param {Number} ratio The relative value of the starting zoom. This will |
| * ALWAYS be 1. |
| * @param {Number} x The X-coordinate of the center of the pinch gesture. |
| * @param {Number} y The Y-coordinate of the center of the pinch gesture. |
| */ |
| this.onzoomstart = null; |
| |
| /** |
| * Called when the amount of zoom changes. |
| * |
| * @event |
| * @param {Number} ratio The relative value of the changed zoom, with 1 |
| * being no change. |
| * @param {Number} x The X-coordinate of the center of the pinch gesture. |
| * @param {Number} y The Y-coordinate of the center of the pinch gesture. |
| */ |
| this.onzoomchange = null; |
| |
| /** |
| * Called when a zoom gesture ends. |
| * |
| * @event |
| * @param {Number} ratio The relative value of the final zoom, with 1 |
| * being no change. |
| * @param {Number} x The X-coordinate of the center of the pinch gesture. |
| * @param {Number} y The Y-coordinate of the center of the pinch gesture. |
| */ |
| this.onzoomend = null; |
| |
| /** |
| * Given a touch event, calculates the distance between the first two |
| * touches in pixels. |
| * |
| * @param {TouchEvent} e The touch event to use when performing distance |
| * calculation. |
| * @return {Number} The distance in pixels between the first two touches. |
| */ |
| function pinch_distance(e) { |
| |
| var touch_a = e.touches[0]; |
| var touch_b = e.touches[1]; |
| |
| var delta_x = touch_a.clientX - touch_b.clientX; |
| var delta_y = touch_a.clientY - touch_b.clientY; |
| |
| return Math.sqrt(delta_x*delta_x + delta_y*delta_y); |
| |
| } |
| |
| /** |
| * Given a touch event, calculates the center between the first two |
| * touches in pixels, returning the X coordinate of this center. |
| * |
| * @param {TouchEvent} e The touch event to use when performing center |
| * calculation. |
| * @return {Number} The X-coordinate of the center of the first two touches. |
| */ |
| function pinch_center_x(e) { |
| |
| var touch_a = e.touches[0]; |
| var touch_b = e.touches[1]; |
| |
| return (touch_a.clientX + touch_b.clientX) / 2; |
| |
| } |
| |
| /** |
| * Given a touch event, calculates the center between the first two |
| * touches in pixels, returning the Y coordinate of this center. |
| * |
| * @param {TouchEvent} e The touch event to use when performing center |
| * calculation. |
| * @return {Number} The Y-coordinate of the center of the first two touches. |
| */ |
| function pinch_center_y(e) { |
| |
| var touch_a = e.touches[0]; |
| var touch_b = e.touches[1]; |
| |
| return (touch_a.clientY + touch_b.clientY) / 2; |
| |
| } |
| |
| // When there are exactly two touches, monitor the distance between |
| // them, firing zoom events as appropriate |
| element.addEventListener("touchmove", function(e) { |
| if (e.touches.length === 2) { |
| |
| e.preventDefault(); |
| e.stopPropagation(); |
| |
| // Calculate current zoom level |
| var current = pinch_distance(e); |
| |
| // Calculate center |
| guac_zoom.centerX = pinch_center_x(e); |
| guac_zoom.centerY = pinch_center_y(e); |
| |
| // If gesture just starting, fire zoom start |
| if (!start_length) { |
| start_length = current; |
| guac_zoom.ratio = 1; |
| if (guac_zoom.onzoomstart) |
| guac_zoom.onzoomstart(guac_zoom.ratio, guac_zoom.centerX, guac_zoom.centerY); |
| } |
| |
| // Otherwise, notify of zoom change |
| else { |
| guac_zoom.ratio = current / start_length; |
| if (guac_zoom.onzoomchange) |
| guac_zoom.onzoomchange(guac_zoom.ratio, guac_zoom.centerX, guac_zoom.centerY); |
| } |
| |
| } |
| }, false); |
| |
| // Reset monitoring and fire end event when done |
| element.addEventListener("touchend", function(e) { |
| |
| if (start_length && e.touches.length < 2) { |
| |
| e.preventDefault(); |
| e.stopPropagation(); |
| |
| start_length = null; |
| if (guac_zoom.onzoomend) |
| guac_zoom.onzoomend(guac_zoom.ratio, guac_zoom.centerX, guac_zoom.centerY); |
| guac_zoom.ratio = 1; |
| } |
| |
| }, false); |
| |
| }; |
| |
| /** |
| * Flattens the attached Guacamole.Client, storing the result within the |
| * connection history. |
| */ |
| GuacUI.Client.updateThumbnail = function() { |
| |
| var guac = GuacUI.Client.attachedClient; |
| if (!guac) |
| return; |
| |
| // Do not create empty thumbnails |
| if (guac.getDisplay().getWidth() <= 0 || guac.getDisplay().getHeight() <= 0) |
| return; |
| |
| // Get screenshot |
| var canvas = guac.getDisplay().flatten(); |
| |
| // Calculate scale of thumbnail (max 320x240, max zoom 100%) |
| var scale = Math.min( |
| 320 / canvas.width, |
| 240 / canvas.height, |
| 1 |
| ); |
| |
| // Create thumbnail canvas |
| var thumbnail = document.createElement("canvas"); |
| thumbnail.width = canvas.width*scale; |
| thumbnail.height = canvas.height*scale; |
| |
| // Scale screenshot to thumbnail |
| var context = thumbnail.getContext("2d"); |
| context.drawImage(canvas, |
| 0, 0, canvas.width, canvas.height, |
| 0, 0, thumbnail.width, thumbnail.height |
| ); |
| |
| // Save thumbnail to history |
| var id = decodeURIComponent(window.location.search.substring(4)); |
| GuacamoleHistory.update(id, thumbnail.toDataURL()); |
| |
| }; |
| |
| /** |
| * Sets the current display scale to the given value, where 1 is 100% (1:1 |
| * pixel ratio). Out-of-range values will be clamped in-range. |
| * |
| * @param {Number} new_scale The new scale to apply |
| */ |
| GuacUI.Client.setScale = function(new_scale) { |
| |
| new_scale = Math.max(new_scale, GuacUI.Client.min_zoom); |
| new_scale = Math.min(new_scale, GuacUI.Client.max_zoom); |
| |
| if (GuacUI.Client.attachedClient) |
| GuacUI.Client.attachedClient.getDisplay().scale(new_scale); |
| |
| GuacUI.Client.zoom_state.textContent = Math.round(new_scale * 100) + "%"; |
| |
| // If at minimum zoom level, auto fit is ON |
| if (new_scale === GuacUI.Client.min_zoom) { |
| GuacUI.Client.main.style.overflow = "hidden"; |
| GuacUI.Client.auto_fit.checked = true; |
| GuacUI.Client.auto_fit.disabled = (GuacUI.Client.min_zoom >= 1); |
| } |
| |
| // If at minimum zoom level, auto fit is OFF |
| else { |
| GuacUI.Client.main.style.overflow = "auto"; |
| GuacUI.Client.auto_fit.checked = false; |
| GuacUI.Client.auto_fit.disabled = false; |
| } |
| |
| }; |
| |
| /** |
| * Updates the scale of the attached Guacamole.Client based on current window |
| * size and "auto-fit" setting. |
| */ |
| GuacUI.Client.updateDisplayScale = function() { |
| |
| var guac = GuacUI.Client.attachedClient; |
| if (!guac) |
| return; |
| |
| // Determine whether display is currently fit to the screen |
| var auto_fit = (guac.getDisplay().getScale() === GuacUI.Client.min_zoom); |
| |
| // Calculate scale to fit screen |
| GuacUI.Client.min_zoom = Math.min( |
| GuacUI.Client.main.offsetWidth / Math.max(guac.getDisplay().getWidth(), 1), |
| GuacUI.Client.main.offsetHeight / Math.max(guac.getDisplay().getHeight(), 1) |
| ); |
| |
| // Calculate appropriate maximum zoom level |
| GuacUI.Client.max_zoom = Math.max(GuacUI.Client.min_zoom, 3); |
| |
| // Clamp zoom level, maintain auto-fit |
| if (guac.getDisplay().getScale() < GuacUI.Client.min_zoom || auto_fit) |
| GuacUI.Client.setScale(GuacUI.Client.min_zoom); |
| |
| else if (guac.getDisplay().getScale() > GuacUI.Client.max_zoom) |
| GuacUI.Client.setScale(GuacUI.Client.max_zoom); |
| |
| }; |
| |
| /** |
| * Updates the document title based on the connection name. |
| */ |
| GuacUI.Client.updateTitle = function () { |
| |
| if (GuacUI.Client.titlePrefix) |
| document.title = GuacUI.Client.titlePrefix + " " + GuacUI.Client.connectionName; |
| else |
| document.title = GuacUI.Client.connectionName; |
| |
| GuacUI.Client.menu_title.textContent = GuacUI.Client.connectionName; |
| |
| }; |
| |
| /** |
| * Sets whether the menu is currently visible. Keyboard is disabled while the |
| * menu is shown. |
| * |
| * @param {Boolean} [shown] Whether the menu should be shown. If omitted, this |
| * function will cause the menu to be shown by default. |
| */ |
| GuacUI.Client.showMenu = function(shown) { |
| if (shown === false) { |
| GuacUI.Client.menu.className = "closed"; |
| GuacUI.Client.commitClipboard(); |
| } |
| else |
| GuacUI.Client.menu.className = "open"; |
| }; |
| |
| /** |
| * Sets whether the text input box is currently visible. |
| * |
| * @param {Boolean} [shown] Whether the text input box should be shown. If |
| * omitted, this function will cause the menu to be |
| * shown by default. |
| */ |
| GuacUI.Client.showTextInput = function(shown) { |
| if (shown === false) { |
| GuacUI.Client.text_input.container.className = "closed"; |
| GuacUI.Client.text_input.target.blur(); |
| } |
| else { |
| GuacUI.Client.text_input.container.className = "open"; |
| GuacUI.Client.text_input.target.focus(); |
| } |
| }; |
| |
| /** |
| * Returns whether the menu is currently shown. |
| * |
| * @returns {Boolean} true if the menu is shown, false otherwise. |
| */ |
| GuacUI.Client.isMenuShown = function() { |
| return GuacUI.Client.menu.className === "open"; |
| }; |
| |
| /** |
| * Hides the currently-visible status overlay, if any. |
| */ |
| GuacUI.Client.hideStatus = function() { |
| if (GuacUI.Client.visibleStatus) |
| GuacUI.Client.visibleStatus.hide(); |
| GuacUI.Client.visibleStatus = null; |
| }; |
| |
| /** |
| * Displays a status overlay with the given text. |
| */ |
| GuacUI.Client.showStatus = function(title, status) { |
| GuacUI.Client.hideStatus(); |
| |
| GuacUI.Client.visibleStatus = new GuacUI.Client.ModalStatus(title, status); |
| GuacUI.Client.visibleStatus.show(); |
| }; |
| |
| /** |
| * Displays an error status overlay with the given text. |
| */ |
| GuacUI.Client.showError = function(title, status, reconnect) { |
| GuacUI.Client.hideStatus(); |
| |
| GuacUI.Client.visibleStatus = |
| new GuacUI.Client.ModalStatus(title, status, "guac-error", reconnect); |
| GuacUI.Client.visibleStatus.show(); |
| }; |
| |
| GuacUI.Client.showNotification = function(message) { |
| |
| // Create notification |
| var element = GuacUI.createElement("div", "message notification"); |
| GuacUI.createChildElement(element, "div", "caption").textContent = message; |
| |
| // Add to DOM |
| GuacUI.Client.notification_area.appendChild(element); |
| |
| // Remove from DOM after around 5 seconds |
| window.setTimeout(function() { |
| GuacUI.Client.notification_area.removeChild(element); |
| }, 5000); |
| |
| }; |
| |
| /** |
| * Connects to the current Guacamole connection, attaching a new Guacamole |
| * client to the user interface. If a Guacamole client is already attached, |
| * it is replaced. |
| */ |
| GuacUI.Client.connect = function() { |
| |
| var tunnel; |
| |
| // If WebSocket available, try to use it. |
| if (window.WebSocket) |
| tunnel = new Guacamole.ChainedTunnel( |
| new Guacamole.WebSocketTunnel("websocket-tunnel"), |
| new Guacamole.HTTPTunnel("tunnel") |
| ); |
| |
| // If no WebSocket, then use HTTP. |
| else |
| tunnel = new Guacamole.HTTPTunnel("tunnel"); |
| |
| // Instantiate client |
| var guac = new Guacamole.Client(tunnel); |
| |
| // Tie UI to client |
| GuacUI.Client.attach(guac); |
| |
| // Calculate optimal width/height for display |
| var pixel_density = window.devicePixelRatio || 1; |
| var optimal_dpi = pixel_density * 96; |
| var optimal_width = window.innerWidth * pixel_density; |
| var optimal_height = window.innerHeight * pixel_density; |
| |
| // Scale width/height to be at least 600x600 |
| if (optimal_width < 600 || optimal_height < 600) { |
| var scale = Math.max(600 / optimal_width, 600 / optimal_height); |
| optimal_width = optimal_width * scale; |
| optimal_height = optimal_height * scale; |
| } |
| |
| // Get entire query string, and pass to connect(). |
| // Normally, only the "id" parameter is required, but |
| // all parameters should be preserved and passed on for |
| // the sake of authentication. |
| |
| var connect_string = |
| window.location.search.substring(1) |
| + "&width=" + Math.floor(optimal_width) |
| + "&height=" + Math.floor(optimal_height) |
| + "&dpi=" + Math.floor(optimal_dpi); |
| |
| // Add audio mimetypes to connect_string |
| GuacUI.Audio.supported.forEach(function(mimetype) { |
| connect_string += "&audio=" + encodeURIComponent(mimetype); |
| }); |
| |
| // Add video mimetypes to connect_string |
| GuacUI.Video.supported.forEach(function(mimetype) { |
| connect_string += "&video=" + encodeURIComponent(mimetype); |
| }); |
| |
| // Show connection errors from tunnel |
| tunnel.onerror = function(status) { |
| var message = GuacUI.Client.tunnel_errors[status.code] || GuacUI.Client.tunnel_errors.DEFAULT; |
| GuacUI.Client.showError("Connection Error", message, |
| GuacUI.Client.tunnel_auto_reconnect[status.code] && GuacUI.Client.RECONNECT_PERIOD); |
| }; |
| |
| // Notify of disconnections (if not already notified of something else) |
| tunnel.onstatechange = function(state) { |
| if (state === Guacamole.Tunnel.State.CLOSED && !GuacUI.Client.visibleStatus) |
| GuacUI.Client.showStatus("Disconnected", "You have been disconnected. Reload the page to reconnect."); |
| }; |
| |
| // Connect |
| guac.connect(connect_string); |
| |
| |
| }; |
| |
| /** |
| * Represents a number of bytes as a human-readable size string, including |
| * units. |
| * |
| * @param {Number} bytes The number of bytes. |
| * @returns {String} A human-readable string containing the size given. |
| */ |
| GuacUI.Client.getSizeString = function(bytes) { |
| |
| if (bytes > 1000000000) |
| return (bytes / 1000000000).toFixed(1) + " GB"; |
| |
| else if (bytes > 1000000) |
| return (bytes / 1000000).toFixed(1) + " MB"; |
| |
| else if (bytes > 1000) |
| return (bytes / 1000).toFixed(1) + " KB"; |
| |
| else |
| return bytes + " B"; |
| |
| }; |
| |
| /** |
| * Commits the current contents of the clipboard textarea to session storage, |
| * and thus to the remote clipboard if the client is connected. |
| */ |
| GuacUI.Client.commitClipboard = function() { |
| var new_value = GuacUI.Client.clipboard.value; |
| GuacamoleSessionStorage.setItem("clipboard", new_value); |
| }; |
| |
| /** |
| * Sets the contents of the remote clipboard, if the contents given are |
| * different. |
| * |
| * @param {String} data The data to assign to the clipboard. |
| */ |
| GuacUI.Client.setClipboard = function(data) { |
| |
| if (data !== GuacUI.Client.remote_clipboard && GuacUI.Client.attachedClient) { |
| GuacUI.Client.remote_clipboard = data; |
| GuacUI.Client.attachedClient.setClipboard(data); |
| } |
| |
| }; |
| |
| /** |
| * Sets the mouse emulation mode to absolute or relative. |
| * |
| * @param {Boolean} absolute Whether mouse emulation should use absolute |
| * (touchscreen) mode. |
| */ |
| GuacUI.Client.setMouseEmulationAbsolute = function(absolute) { |
| |
| function __handle_mouse_state(mouseState) { |
| |
| // Get client - do nothing if not attached |
| var guac = GuacUI.Client.attachedClient; |
| if (!guac) return; |
| |
| // Determine mouse position within view |
| var guac_display = guac.getDisplay().getElement(); |
| var mouse_view_x = mouseState.x + guac_display.offsetLeft - GuacUI.Client.main.scrollLeft; |
| var mouse_view_y = mouseState.y + guac_display.offsetTop - GuacUI.Client.main.scrollTop; |
| |
| // Determine viewport dimensioins |
| var view_width = GuacUI.Client.main.offsetWidth; |
| var view_height = GuacUI.Client.main.offsetHeight; |
| |
| // Determine scroll amounts based on mouse position relative to document |
| |
| var scroll_amount_x; |
| if (mouse_view_x > view_width) |
| scroll_amount_x = mouse_view_x - view_width; |
| else if (mouse_view_x < 0) |
| scroll_amount_x = mouse_view_x; |
| else |
| scroll_amount_x = 0; |
| |
| var scroll_amount_y; |
| if (mouse_view_y > view_height) |
| scroll_amount_y = mouse_view_y - view_height; |
| else if (mouse_view_y < 0) |
| scroll_amount_y = mouse_view_y; |
| else |
| scroll_amount_y = 0; |
| |
| // Scroll (if necessary) to keep mouse on screen. |
| GuacUI.Client.main.scrollLeft += scroll_amount_x; |
| GuacUI.Client.main.scrollTop += scroll_amount_y; |
| |
| // Scale event by current scale |
| var scaledState = new Guacamole.Mouse.State( |
| mouseState.x / guac.getDisplay().getScale(), |
| mouseState.y / guac.getDisplay().getScale(), |
| mouseState.left, |
| mouseState.middle, |
| mouseState.right, |
| mouseState.up, |
| mouseState.down); |
| |
| // Send mouse event |
| guac.sendMouseState(scaledState); |
| |
| }; |
| |
| var new_mode, old_mode; |
| GuacUI.Client.emulate_absolute = absolute; |
| |
| // Switch to touchscreen if absolute |
| if (absolute) { |
| new_mode = GuacUI.Client.touch_screen; |
| old_mode = GuacUI.Client.touch; |
| } |
| |
| // Switch to touchpad if not absolute (relative) |
| else { |
| new_mode = GuacUI.Client.touch_pad; |
| old_mode = GuacUI.Client.touch; |
| } |
| |
| // Perform switch |
| if (new_mode) { |
| |
| if (old_mode) { |
| old_mode.onmousedown = old_mode.onmouseup = old_mode.onmousemove = null; |
| new_mode.currentState.x = old_mode.currentState.x; |
| new_mode.currentState.y = old_mode.currentState.y; |
| } |
| |
| new_mode.onmousedown = new_mode.onmouseup = new_mode.onmousemove = __handle_mouse_state; |
| GuacUI.Client.touch = new_mode; |
| } |
| |
| }; |
| |
| /** |
| * Attaches a Guacamole.Client to the client UI, such that Guacamole events |
| * affect the UI, and local events affect the Guacamole.Client. If a client |
| * is already attached, it is replaced. |
| * |
| * @param {Guacamole.Client} guac The Guacamole.Client to attach to the UI. |
| */ |
| GuacUI.Client.attach = function(guac) { |
| |
| // If a client is already attached, ensure it is disconnected |
| if (GuacUI.Client.attachedClient) |
| GuacUI.Client.attachedClient.disconnect(); |
| |
| // Store attached client |
| GuacUI.Client.attachedClient = guac; |
| |
| // Get display element |
| var guac_display = guac.getDisplay().getElement(); |
| |
| /* |
| * Update the scale of the display when the client display size changes. |
| */ |
| |
| guac.getDisplay().onresize = function(width, height) { |
| GuacUI.Client.updateDisplayScale(); |
| }; |
| |
| /* |
| * Update UI when the state of the Guacamole.Client changes. |
| */ |
| |
| guac.onstatechange = function(clientState) { |
| |
| switch (clientState) { |
| |
| // Idle |
| case 0: |
| GuacUI.Client.showStatus(null, "Idle."); |
| GuacUI.Client.titlePrefix = "[Idle]"; |
| break; |
| |
| // Connecting |
| case 1: |
| GuacUI.Client.showStatus("Connecting", "Connecting to Guacamole..."); |
| GuacUI.Client.titlePrefix = "[Connecting...]"; |
| break; |
| |
| // Connected + waiting |
| case 2: |
| GuacUI.Client.showStatus("Connecting", "Connected to Guacamole. Waiting for response..."); |
| GuacUI.Client.titlePrefix = "[Waiting...]"; |
| break; |
| |
| // Connected |
| case 3: |
| |
| GuacUI.Client.hideStatus(); |
| GuacUI.Client.titlePrefix = null; |
| |
| // Update clipboard with current data |
| var clipboard = GuacamoleSessionStorage.getItem("clipboard"); |
| if (clipboard) |
| GuacUI.Client.setClipboard(clipboard); |
| |
| break; |
| |
| // Disconnecting / disconnected are handled by tunnel instead |
| case 4: |
| case 5: |
| break; |
| |
| // Unknown status code |
| default: |
| GuacUI.Client.showStatus("Unknown Status", "An unknown status code was received. This is most likely a bug."); |
| |
| } |
| |
| GuacUI.Client.updateTitle(); |
| |
| }; |
| |
| /* |
| * Change UI to reflect the connection name |
| */ |
| |
| guac.onname = function(name) { |
| GuacUI.Client.connectionName = name; |
| GuacUI.Client.updateTitle(); |
| }; |
| |
| /* |
| * Disconnect and display an error message when the Guacamole.Client |
| * receives an error. |
| */ |
| |
| guac.onerror = function(status) { |
| |
| // Disconnect, if connected |
| guac.disconnect(); |
| |
| // Display error message |
| var message = GuacUI.Client.client_errors[status.code] || GuacUI.Client.client_errors.DEFAULT; |
| GuacUI.Client.showError("Connection Error", message, |
| GuacUI.Client.client_auto_reconnect[status.code] && GuacUI.Client.RECONNECT_PERIOD); |
| |
| }; |
| |
| // Server copy handler |
| guac.onclipboard = function(stream, mimetype) { |
| |
| // Only text/plain is supported for now |
| if (mimetype !== "text/plain") { |
| stream.sendAck("Only text/plain supported", Guacamole.Status.Code.UNSUPPORTED); |
| return; |
| } |
| |
| var reader = new Guacamole.StringReader(stream); |
| var data = ""; |
| |
| // Append any received data to buffer |
| reader.ontext = function clipboard_text_received(text) { |
| data += text; |
| stream.sendAck("Received", Guacamole.Status.Code.SUCCESS); |
| }; |
| |
| // Set contents when done |
| reader.onend = function clipboard_text_end() { |
| GuacUI.Client.remote_clipboard = data; |
| GuacamoleSessionStorage.setItem("clipboard", data); |
| }; |
| |
| }; |
| |
| /* |
| * Prompt to download file when file received. |
| */ |
| |
| guac.onfile = function(stream, mimetype, filename) { |
| |
| var download = new GuacUI.Download(filename); |
| download.updateProgress(GuacUI.Client.getSizeString(0)); |
| |
| var blob_reader = new Guacamole.BlobReader(stream, mimetype); |
| |
| GuacUI.Client.notification_area.appendChild(download.getElement()); |
| |
| // Update progress as data is received |
| blob_reader.onprogress = function() { |
| download.updateProgress(GuacUI.Client.getSizeString(blob_reader.getLength())); |
| stream.sendAck("Received", 0x0000); |
| }; |
| |
| // When complete, prompt for download |
| blob_reader.onend = function() { |
| |
| download.ondownload = function() { |
| saveAs(blob_reader.getBlob(), filename); |
| }; |
| |
| download.complete(); |
| |
| }; |
| |
| // When close clicked, remove from notification area |
| download.onclose = function() { |
| GuacUI.Client.notification_area.removeChild(download.getElement()); |
| }; |
| |
| stream.sendAck("Ready", 0x0000); |
| |
| }; |
| |
| /* |
| * Do nothing when the display element is clicked on. |
| */ |
| |
| guac_display.onclick = function(e) { |
| e.preventDefault(); |
| return false; |
| }; |
| |
| /* |
| * Handle mouse and touch events relative to the display element. |
| */ |
| |
| // Touchscreen |
| var touch_screen = new Guacamole.Mouse.Touchscreen(guac_display); |
| GuacUI.Client.touch_screen = touch_screen; |
| |
| // Touchpad |
| var touch_pad = new Guacamole.Mouse.Touchpad(guac_display); |
| GuacUI.Client.touch_pad = touch_pad; |
| |
| // Init emulation mode for client |
| GuacUI.Client.setMouseEmulationAbsolute(GuacUI.Client.absolute_radio.checked); |
| |
| // Mouse |
| var mouse = new Guacamole.Mouse(guac_display); |
| mouse.onmousedown = mouse.onmouseup = mouse.onmousemove = function(mouseState) { |
| |
| // Scale event by current scale |
| var scaledState = new Guacamole.Mouse.State( |
| mouseState.x / guac.getDisplay().getScale(), |
| mouseState.y / guac.getDisplay().getScale(), |
| mouseState.left, |
| mouseState.middle, |
| mouseState.right, |
| mouseState.up, |
| mouseState.down); |
| |
| // Send mouse event |
| guac.sendMouseState(scaledState); |
| |
| }; |
| |
| |
| // Hide any existing status notifications |
| GuacUI.Client.hideStatus(); |
| |
| // Remove old client from UI, if any |
| GuacUI.Client.display.innerHTML = ""; |
| |
| // Add client to UI |
| guac.getDisplay().getElement().className = "software-cursor"; |
| GuacUI.Client.display.appendChild(guac.getDisplay().getElement()); |
| |
| }; |
| |
| // One-time UI initialization |
| (function() { |
| |
| var i; |
| |
| /** |
| * Keys which should be allowed through to the client when in text input |
| * mode, providing corresponding key events are received. Keys in this |
| * set will be allowed through to the server. |
| */ |
| var IME_ALLOWED_KEYS = { |
| 0xFF08: true, /* Backspace */ |
| 0xFF09: true, /* Tab */ |
| 0xFF0D: true, /* Enter */ |
| 0xFF1B: true, /* Escape */ |
| 0xFF50: true, /* Home */ |
| 0xFF51: true, /* Left */ |
| 0xFF52: true, /* Up */ |
| 0xFF53: true, /* Right */ |
| 0xFF54: true, /* Down */ |
| 0xFF57: true, /* End */ |
| 0xFF64: true, /* Insert */ |
| 0xFFBE: true, /* F1 */ |
| 0xFFBF: true, /* F2 */ |
| 0xFFC0: true, /* F3 */ |
| 0xFFC1: true, /* F4 */ |
| 0xFFC2: true, /* F5 */ |
| 0xFFC3: true, /* F6 */ |
| 0xFFC4: true, /* F7 */ |
| 0xFFC5: true, /* F8 */ |
| 0xFFC6: true, /* F9 */ |
| 0xFFC7: true, /* F10 */ |
| 0xFFC8: true, /* F11 */ |
| 0xFFC9: true, /* F12 */ |
| 0xFFE1: true, /* Left shift */ |
| 0xFFE2: true, /* Right shift */ |
| 0xFFFF: true /* Delete */ |
| }; |
| |
| /* |
| * Route document-level keyboard events to the client. |
| */ |
| |
| var keyboard = new Guacamole.Keyboard(document); |
| var show_keyboard_gesture_possible = true; |
| |
| function __send_key(pressed, keysym) { |
| |
| // Do not send key if menu shown |
| if (GuacUI.Client.isMenuShown()) |
| return true; |
| |
| // Allow all but specific keys through to browser when in IME mode |
| if (GuacUI.Client.text_input.enabled && !IME_ALLOWED_KEYS[keysym]) |
| return true; |
| |
| GuacUI.Client.attachedClient.sendKeyEvent(pressed, keysym); |
| return false; |
| |
| } |
| |
| keyboard.onkeydown = function (keysym) { |
| |
| // Only handle key events if client is attached |
| var guac = GuacUI.Client.attachedClient; |
| if (!guac) return true; |
| |
| // Handle Ctrl-shortcuts specifically |
| if (keyboard.modifiers.ctrl && !keyboard.modifiers.alt && !keyboard.modifiers.shift) { |
| |
| // Allow event through if Ctrl+C or Ctrl+X |
| if (keyboard.pressed[0x63] || keyboard.pressed[0x78]) { |
| __send_key(1, keysym); |
| return true; |
| } |
| |
| // If Ctrl+V, wait until after paste event (next event loop) |
| if (keyboard.pressed[0x76]) { |
| window.setTimeout(function after_paste() { |
| __send_key(1, keysym); |
| }, 10); |
| return true; |
| } |
| |
| } |
| |
| // If key is NOT one of the expected keys, gesture not possible |
| if (keysym !== 0xFFE3 && keysym !== 0xFFE9 && keysym !== 0xFFE1) |
| show_keyboard_gesture_possible = false; |
| |
| // Send key event |
| return __send_key(1, keysym); |
| |
| }; |
| |
| keyboard.onkeyup = function (keysym) { |
| |
| // Only handle key events if client is attached |
| var guac = GuacUI.Client.attachedClient; |
| if (!guac) return true; |
| |
| // If lifting up on shift, toggle menu visibility if rest of gesture |
| // conditions satisfied |
| if (show_keyboard_gesture_possible && keysym === 0xFFE1 |
| && keyboard.pressed[0xFFE3] && keyboard.pressed[0xFFE9]) { |
| __send_key(0, 0xFFE1); |
| __send_key(0, 0xFFE9); |
| __send_key(0, 0xFFE3); |
| GuacUI.Client.showMenu(!GuacUI.Client.isMenuShown()); |
| } |
| |
| // Detect if no keys are pressed |
| var reset_gesture = true; |
| for (var pressed in keyboard.pressed) { |
| reset_gesture = false; |
| break; |
| } |
| |
| // Reset gesture state if possible |
| if (reset_gesture) |
| show_keyboard_gesture_possible = true; |
| |
| // Send key event |
| return __send_key(0, keysym); |
| |
| }; |
| |
| /** |
| * Returns the contents of the remote clipboard if clipboard integration is |
| * enabled, and null otherwise. |
| */ |
| function get_clipboard_data() { |
| |
| // If integration not enabled, do not attempt retrieval |
| if (GuacUI.Client.clipboard_integration_enabled === false) |
| return null; |
| |
| // Otherwise, attempt retrieval and update integration status |
| try { |
| var data = GuacamoleService.Clipboard.get(); |
| GuacUI.Client.clipboard_integration_enabled = true; |
| return data; |
| } |
| catch (status) { |
| GuacUI.Client.clipboard_integration_enabled = false; |
| return null; |
| } |
| |
| } |
| |
| // Set local clipboard contents on cut |
| document.body.addEventListener("cut", function handle_cut(e) { |
| var data = get_clipboard_data(); |
| if (data !== null) { |
| e.preventDefault(); |
| e.clipboardData.setData("text/plain", data); |
| } |
| }, false); |
| |
| // Set local clipboard contents on copy |
| document.body.addEventListener("copy", function handle_copy(e) { |
| var data = get_clipboard_data(); |
| if (data !== null) { |
| e.preventDefault(); |
| e.clipboardData.setData("text/plain", data); |
| } |
| }, false); |
| |
| // Set remote clipboard contents on paste |
| document.body.addEventListener("paste", function handle_paste(e) { |
| |
| // If status of clipboard integration is unknown, attempt to define it |
| if (GuacUI.Client.clipboard_integration_enabled === undefined) |
| get_clipboard_data(); |
| |
| // Override and handle paste only if integration is enabled |
| if (GuacUI.Client.clipboard_integration_enabled) { |
| e.preventDefault(); |
| GuacUI.Client.setClipboard(e.clipboardData.getData("text/plain")); |
| } |
| |
| }, false); |
| |
| /* |
| * Disconnect and update thumbnail on close |
| */ |
| window.onunload = function() { |
| |
| GuacUI.Client.updateThumbnail(); |
| |
| if (GuacUI.Client.attachedClient) |
| GuacUI.Client.attachedClient.disconnect(); |
| |
| }; |
| |
| /* |
| * Reflow layout and send size events on resize/scroll |
| */ |
| |
| var last_scroll_left = 0; |
| var last_scroll_top = 0; |
| var last_scroll_width = 0; |
| var last_scroll_height = 0; |
| var last_window_width = 0; |
| var last_window_height = 0; |
| |
| function __update_layout() { |
| |
| // Only reflow if size or scroll have changed |
| if (document.body.scrollLeft !== last_scroll_left |
| || document.body.scrollTop !== last_scroll_top |
| || document.body.scrollWidth !== last_scroll_width |
| || document.body.scrollHeight !== last_scroll_height |
| || window.innerWidth !== last_window_width |
| || window.innerHeight !== last_window_height) { |
| |
| last_scroll_top = document.body.scrollTop; |
| last_scroll_left = document.body.scrollLeft; |
| last_scroll_width = document.body.scrollWidth; |
| last_scroll_height = document.body.scrollHeight; |
| last_window_width = window.innerWidth; |
| last_window_height = window.innerHeight; |
| |
| // Reset scroll and reposition document such that it's on-screen |
| window.scrollTo(document.body.scrollWidth, document.body.scrollHeight); |
| |
| // Determine height of bottom section (currently only text input) |
| var bottom = GuacUI.Client.text_input.container; |
| var bottom_height = (bottom && bottom.offsetHeight) | 0; |
| |
| // Calculate correct height of main section (display) |
| var main_width = window.innerWidth; |
| var main_height = window.innerHeight - bottom_height; |
| |
| // Anchor main to top-left of viewport, sized to fit above bottom |
| var main = GuacUI.Client.main; |
| main.style.top = document.body.scrollTop + "px"; |
| main.style.left = document.body.scrollLeft + "px"; |
| main.style.width = main_width + "px"; |
| main.style.height = main_height + "px"; |
| |
| // Anchor bottom to bottom of viewport |
| if (bottom) { |
| bottom.style.top = (document.body.scrollTop + main_height) + "px"; |
| bottom.style.left = document.body.scrollLeft + "px"; |
| bottom.style.width = window.innerWidth + "px"; |
| } |
| |
| // Send new size |
| if (GuacUI.Client.attachedClient) { |
| var pixel_density = window.devicePixelRatio || 1; |
| var width = main_width * pixel_density; |
| var height = main_height * pixel_density; |
| GuacUI.Client.attachedClient.sendSize(width, height); |
| } |
| |
| // Rescale display appropriately |
| GuacUI.Client.updateDisplayScale(); |
| |
| } |
| |
| } |
| |
| window.onresize = __update_layout; |
| window.onscroll = __update_layout; |
| window.setInterval(__update_layout, 10); |
| |
| GuacamoleSessionStorage.addChangeListener(function(name, value) { |
| if (name === "clipboard") { |
| GuacUI.Client.clipboard.value = value; |
| GuacUI.Client.setClipboard(value); |
| } |
| }); |
| |
| /** |
| * Ignores the given event. |
| * |
| * @private |
| * @param {Event} e The event to ignore. |
| */ |
| function _ignore(e) { |
| e.preventDefault(); |
| e.stopPropagation(); |
| } |
| |
| /** |
| * Converts the given bytes to a base64-encoded string. |
| * |
| * @private |
| * @param {Uint8Array} bytes A Uint8Array which contains the data to be |
| * encoded as base64. |
| * @return {String} The base64-encoded string. |
| */ |
| function _get_base64(bytes) { |
| |
| var data = ""; |
| |
| // Produce binary string from bytes in buffer |
| for (var i=0; i<bytes.byteLength; i++) |
| data += String.fromCharCode(bytes[i]); |
| |
| // Convert to base64 |
| return window.btoa(data); |
| |
| } |
| |
| /** |
| * Uploads the given file to the server. |
| * |
| * @private |
| * @param {File} file The file to upload. |
| */ |
| function _upload_file(file) { |
| |
| // Construct reader for file |
| var reader = new FileReader(); |
| reader.onloadend = function() { |
| |
| // Add upload notification |
| var upload = new GuacUI.Upload(file.name); |
| upload.updateProgress(GuacUI.Client.getSizeString(0), 0); |
| |
| GuacUI.Client.notification_area.appendChild(upload.getElement()); |
| |
| // Open file for writing |
| var stream = GuacUI.Client.attachedClient.createFileStream(file.type, file.name); |
| |
| var valid = true; |
| var bytes = new Uint8Array(reader.result); |
| var offset = 0; |
| |
| // Invalidate stream on all errors |
| // Continue upload when acknowledged |
| stream.onack = function(status) { |
| |
| // Handle errors |
| if (status.isError()) { |
| valid = false; |
| var message = GuacUI.Client.upload_errors[status.code] |
| || GuacUI.Client.upload_errors.DEFAULT; |
| upload.showError(message); |
| } |
| |
| // Abort upload if stream is invalid |
| if (!valid) return false; |
| |
| // Encode packet as base64 |
| var slice = bytes.subarray(offset, offset+4096); |
| var base64 = _get_base64(slice); |
| |
| // Write packet |
| stream.sendBlob(base64); |
| |
| // Advance to next packet |
| offset += 4096; |
| |
| // If at end, stop upload |
| if (offset >= bytes.length) { |
| stream.sendEnd(); |
| GuacUI.Client.notification_area.removeChild(upload.getElement()); |
| GuacUI.Client.showNotification("Upload of \"" + file.name + "\" complete."); |
| } |
| |
| // Otherwise, update progress |
| else |
| upload.updateProgress(GuacUI.Client.getSizeString(offset), offset / bytes.length * 100); |
| |
| }; |
| |
| // Close dialog and abort when close is clicked |
| upload.onclose = function() { |
| GuacUI.Client.notification_area.removeChild(upload.getElement()); |
| // TODO: Abort transfer |
| }; |
| |
| }; |
| reader.readAsArrayBuffer(file); |
| |
| } |
| |
| // Handle and ignore dragenter/dragover |
| GuacUI.Client.display.addEventListener("dragenter", _ignore, false); |
| GuacUI.Client.display.addEventListener("dragover", _ignore, false); |
| |
| // File drop event handler |
| GuacUI.Client.display.addEventListener("drop", function(e) { |
| |
| e.preventDefault(); |
| e.stopPropagation(); |
| |
| // Ignore file drops if no attached client |
| if (!GuacUI.Client.attachedClient) return; |
| |
| // Upload each file |
| var files = e.dataTransfer.files; |
| for (var i=0; i<files.length; i++) |
| _upload_file(files[i]); |
| |
| }, false); |
| |
| /* |
| * Pinch-to-zoom |
| */ |
| |
| var guac_pinch = new GuacUI.Client.Pinch(document.body); |
| var initial_scale = null; |
| var initial_center_x = null; |
| var initial_center_y = null; |
| |
| guac_pinch.onzoomstart = function(ratio, x, y) { |
| |
| var guac = GuacUI.Client.attachedClient; |
| if (!guac) |
| return; |
| |
| initial_scale = guac.getDisplay().getScale(); |
| initial_center_x = (x + GuacUI.Client.main.scrollLeft) / initial_scale; |
| initial_center_y = (y + GuacUI.Client.main.scrollTop) / initial_scale; |
| }; |
| |
| guac_pinch.onzoomchange = function(ratio, x, y) { |
| |
| var guac = GuacUI.Client.attachedClient; |
| if (!guac) |
| return; |
| |
| // Ignore pinch for relative mouse emulation |
| if (!GuacUI.Client.emulate_absolute) |
| return; |
| |
| // Rescale based on new ratio |
| var new_scale = initial_scale * ratio; |
| GuacUI.Client.setScale(new_scale); |
| |
| // Calculate point at currently at center of touch |
| var point_at_center_x = (x + GuacUI.Client.main.scrollLeft) / new_scale; |
| var point_at_center_y = (y + GuacUI.Client.main.scrollTop) / new_scale; |
| |
| // Correct position to keep point-of-interest within center of pinch |
| GuacUI.Client.main.scrollLeft += (initial_center_x - point_at_center_x) * new_scale; |
| GuacUI.Client.main.scrollTop += (initial_center_y - point_at_center_y) * new_scale; |
| |
| }; |
| |
| /* |
| * Touch panning/swiping |
| */ |
| |
| var guac_drag = new GuacUI.Client.Drag(document.body); |
| |
| var is_swipe_right = false; |
| var drag_start = 0; |
| var last_drag_dx = 0; |
| var last_drag_dy = 0; |
| |
| guac_drag.ondragstart = function(dx, dy) { |
| |
| last_drag_dx = dx; |
| last_drag_dy = dy; |
| drag_start = new Date().getTime(); |
| |
| // If dragging from far left, consider gesture to be a swipe |
| is_swipe_right = (guac_drag.start_x <= 32 && (!GuacUI.Client.touch || !GuacUI.Client.touch.currentState.left)); |
| |
| }; |
| |
| guac_drag.ondragchange = function(dx, dy) { |
| if (!GuacUI.Client.touch || !GuacUI.Client.touch.currentState.left) { |
| |
| var duration = new Date().getTime() - drag_start; |
| var change_drag_dx = dx - last_drag_dx; |
| var change_drag_dy = dy - last_drag_dy; |
| |
| // Show menu if swiping right |
| if (is_swipe_right && !GuacUI.Client.isMenuShown()) { |
| if (dx >= 64 && Math.abs(dy) < 32 && duration < 250) { |
| GuacUI.Client.showMenu(); |
| guac_drag.cancel(); |
| } |
| } |
| |
| // Hide menu if swiping left |
| else if (GuacUI.Client.isMenuShown()) { |
| |
| GuacUI.Client.menu.scrollLeft -= change_drag_dx; |
| GuacUI.Client.menu.scrollTop -= change_drag_dy; |
| |
| if (dx <= -64 && Math.abs(dy) < 32 && duration < 250) { |
| GuacUI.Client.showMenu(false); |
| guac_drag.cancel(); |
| } |
| |
| } |
| |
| // Otherwise, drag UI (if not relative emulation) |
| else if (GuacUI.Client.emulate_absolute) { |
| GuacUI.Client.main.scrollLeft -= change_drag_dx; |
| GuacUI.Client.main.scrollTop -= change_drag_dy; |
| } |
| |
| last_drag_dx = dx; |
| last_drag_dy = dy; |
| |
| } |
| }; |
| |
| /* |
| * Initialize clipboard with current data |
| */ |
| |
| GuacUI.Client.clipboard.value = GuacamoleSessionStorage.getItem("clipboard", ""); |
| |
| /* |
| * Update clipboard contents when changed |
| */ |
| |
| window.onblur = |
| GuacUI.Client.clipboard.onchange = function() { |
| GuacUI.Client.commitClipboard(); |
| }; |
| |
| /* |
| * Update emulation mode when changed |
| */ |
| |
| GuacUI.Client.absolute_radio.onclick = |
| GuacUI.Client.absolute_radio.onchange = function() { |
| if (!GuacUI.Client.emulate_absolute) { |
| GuacUI.Client.showNotification("Absolute mouse emulation selected"); |
| GuacUI.Client.setMouseEmulationAbsolute(GuacUI.Client.absolute_radio.checked); |
| GuacUI.Client.showMenu(false); |
| } |
| }; |
| |
| GuacUI.Client.relative_radio.onclick = |
| GuacUI.Client.relative_radio.onchange = function() { |
| if (GuacUI.Client.emulate_absolute) { |
| GuacUI.Client.showNotification("Relative mouse emulation selected"); |
| GuacUI.Client.setMouseEmulationAbsolute(!GuacUI.Client.relative_radio.checked); |
| GuacUI.Client.showMenu(false); |
| } |
| }; |
| |
| /* |
| * Update input method mode when changed |
| */ |
| |
| GuacUI.Client.ime_none_radio.onclick = |
| GuacUI.Client.ime_none_radio.onchange = function() { |
| GuacUI.Client.showTextInput(false); |
| GuacUI.Client.OnScreenKeyboard.hide(); |
| GuacUI.Client.showMenu(false); |
| }; |
| |
| GuacUI.Client.ime_text_radio.onclick = |
| GuacUI.Client.ime_text_radio.onchange = function() { |
| GuacUI.Client.showTextInput(true); |
| GuacUI.Client.OnScreenKeyboard.hide(); |
| GuacUI.Client.showMenu(false); |
| }; |
| |
| GuacUI.Client.ime_osk_radio.onclick = |
| GuacUI.Client.ime_osk_radio.onchange = function() { |
| GuacUI.Client.showTextInput(false); |
| GuacUI.Client.OnScreenKeyboard.show(); |
| GuacUI.Client.showMenu(false); |
| }; |
| |
| /* |
| * Text input |
| */ |
| |
| // Disable automatic input features on platforms that support these attributes |
| GuacUI.Client.text_input.target.setAttribute("autocapitalize", "off"); |
| GuacUI.Client.text_input.target.setAttribute("autocorrect", "off"); |
| GuacUI.Client.text_input.target.setAttribute("autocomplete", "off"); |
| GuacUI.Client.text_input.target.setAttribute("spellcheck", "off"); |
| |
| function keysym_from_codepoint(codepoint) { |
| |
| // Keysyms for control characters |
| if (codepoint <= 0x1F || (codepoint >= 0x7F && codepoint <= 0x9F)) |
| return 0xFF00 | codepoint; |
| |
| // Keysyms for ASCII chars |
| if (codepoint >= 0x0000 && codepoint <= 0x00FF) |
| return codepoint; |
| |
| // Keysyms for Unicode |
| if (codepoint >= 0x0100 && codepoint <= 0x10FFFF) |
| return 0x01000000 | codepoint; |
| |
| return null; |
| |
| } |
| |
| /** |
| * Presses and releases the key corresponding to the given keysym, as if |
| * typed by the user. |
| * |
| * @param {Number} keysym The keysym of the key to send. |
| */ |
| function send_keysym(keysym) { |
| |
| var guac = GuacUI.Client.attachedClient; |
| if (!guac) |
| return; |
| |
| guac.sendKeyEvent(1, keysym); |
| guac.sendKeyEvent(0, keysym); |
| |
| } |
| |
| /** |
| * Presses and releases the key having the keysym corresponding to the |
| * Unicode codepoint given, as if typed by the user. |
| * |
| * @param {Number} codepoint The Unicode codepoint of the key to send. |
| */ |
| function send_codepoint(codepoint) { |
| |
| if (codepoint === 10) { |
| send_keysym(0xFF0D); |
| release_sticky_keys(); |
| return; |
| } |
| |
| var keysym = keysym_from_codepoint(codepoint); |
| if (keysym) { |
| send_keysym(keysym); |
| release_sticky_keys(); |
| } |
| |
| } |
| |
| /** |
| * Translates each character within the given string to keysyms and sends |
| * each, in order, as if typed by the user. |
| * |
| * @param {String} content The string to send. |
| */ |
| function send_string(content) { |
| |
| var sent_text = ""; |
| |
| for (var i=0; i<content.length; i++) { |
| var codepoint = content.charCodeAt(i); |
| if (codepoint !== GuacUI.Client.TEXT_INPUT_PADDING_CODEPOINT) { |
| sent_text += String.fromCharCode(codepoint); |
| send_codepoint(codepoint); |
| } |
| } |
| |
| // Display the text that was sent |
| var notify_sent = GuacUI.createChildElement(GuacUI.Client.text_input.sent, "div", "sent-text"); |
| notify_sent.textContent = sent_text; |
| |
| // Remove text after one second |
| window.setTimeout(function __remove_notify_sent() { |
| notify_sent.parentNode.removeChild(notify_sent); |
| }, 1000); |
| |
| } |
| |
| /** |
| * Set of all active key elements, indexed by keysym. |
| * |
| * @private |
| * @type Object.<Number, Element> |
| */ |
| var active_sticky_keys = {}; |
| |
| /** |
| * Presses/releases the keysym defined by the "data-keysym" attribute on |
| * the given element whenever the element is pressed. The "data-sticky" |
| * attribute, if present and set to "true", causes the key to remain |
| * pressed until text is sent. |
| * |
| * @param {Element} key The element which will control its associated key. |
| */ |
| function apply_key_behavior(key) { |
| |
| function __update_key(e) { |
| |
| var guac = GuacUI.Client.attachedClient; |
| if (!guac) |
| return; |
| |
| e.preventDefault(); |
| e.stopPropagation(); |
| |
| // Pull properties of key |
| var keysym = parseInt(key.getAttribute("data-keysym")); |
| var sticky = (key.getAttribute("data-sticky") === "true"); |
| var pressed = (key.className.indexOf("pressed") !== -1); |
| |
| // If sticky, toggle pressed state |
| if (sticky) { |
| if (pressed) { |
| GuacUI.removeClass(key, "pressed"); |
| guac.sendKeyEvent(0, keysym); |
| delete active_sticky_keys[keysym]; |
| } |
| else { |
| GuacUI.addClass(key, "pressed"); |
| guac.sendKeyEvent(1, keysym); |
| active_sticky_keys[keysym] = key; |
| } |
| } |
| |
| // For all non-sticky keys, press and release key immediately |
| else |
| send_keysym(keysym); |
| |
| } |
| |
| var ignore_mouse = false; |
| |
| // Press/release key when clicked |
| key.addEventListener("click", function __mouse_key(e) { |
| |
| // Ignore clicks which follow touches |
| if (ignore_mouse) |
| return; |
| |
| __update_key(e); |
| |
| }, false); |
| |
| // Press/release key when tapped |
| key.addEventListener("touchstart", function __touch_key(e) { |
| |
| // Ignore following clicks |
| ignore_mouse = true; |
| |
| __update_key(e); |
| |
| }, false); |
| |
| // Restore handling of mouse events when mouse is used |
| key.addEventListener("mousemove", function __reset_mouse() { |
| ignore_mouse = false; |
| }, false); |
| |
| } |
| |
| /** |
| * Releases all currently-held sticky keys within the text input UI. |
| */ |
| function release_sticky_keys() { |
| |
| var guac = GuacUI.Client.attachedClient; |
| if (!guac) |
| return; |
| |
| // Release all active sticky keys |
| for (var keysym in active_sticky_keys) { |
| var key = active_sticky_keys[keysym]; |
| GuacUI.removeClass(key, "pressed"); |
| guac.sendKeyEvent(0, keysym); |
| } |
| |
| // Reset set of active keys |
| active_sticky_keys = {}; |
| |
| } |
| |
| // Apply key behavior to all keys within the text input UI |
| var keys = GuacUI.Client.text_input.container.getElementsByClassName("key"); |
| for (i=0; i<keys.length; i++) |
| apply_key_behavior(keys[i]); |
| |
| /** |
| * Removes all content from the text input target, replacing it with the |
| * given number of padding characters. Padding of the requested size is |
| * added on both sides of the cursor, thus the overall number of characters |
| * added will be twice the number specified. |
| * |
| * @param {Number} padding The number of characters to pad the text area |
| * with. |
| */ |
| function reset_text_input_target(padding) { |
| |
| var padding_char = String.fromCharCode(GuacUI.Client.TEXT_INPUT_PADDING_CODEPOINT); |
| |
| // Pad text area with an arbitrary, non-typable character (so there is something |
| // to delete with backspace or del), and position cursor in middle. |
| GuacUI.Client.text_input.target.value = new Array(padding*2 + 1).join(padding_char); |
| GuacUI.Client.text_input.target.setSelectionRange(padding, padding); |
| |
| } |
| |
| GuacUI.Client.text_input.target.onfocus = function() { |
| GuacUI.Client.text_input.enabled = true; |
| reset_text_input_target(GuacUI.Client.TEXT_INPUT_PADDING); |
| }; |
| |
| GuacUI.Client.text_input.target.onblur = function() { |
| GuacUI.Client.text_input.enabled = false; |
| GuacUI.Client.text_input.target.focus(); |
| }; |
| |
| // Track state of composition |
| var composing_text = false; |
| |
| GuacUI.Client.text_input.target.addEventListener("compositionstart", function(e) { |
| composing_text = true; |
| }, false); |
| |
| GuacUI.Client.text_input.target.addEventListener("compositionend", function(e) { |
| composing_text = false; |
| }, false); |
| |
| GuacUI.Client.text_input.target.addEventListener("input", function(e) { |
| |
| // Ignore input events during text composition |
| if (composing_text) |
| return; |
| |
| var i; |
| var content = GuacUI.Client.text_input.target.value; |
| var expected_length = GuacUI.Client.TEXT_INPUT_PADDING*2; |
| |
| // If content removed, update |
| if (content.length < expected_length) { |
| |
| // Calculate number of backspaces and send |
| var backspace_count = GuacUI.Client.TEXT_INPUT_PADDING - GuacUI.Client.text_input.target.selectionStart; |
| for (i=0; i<backspace_count; i++) |
| send_keysym(0xFF08); |
| |
| // Calculate number of deletes and send |
| var delete_count = expected_length - content.length - backspace_count; |
| for (i=0; i<delete_count; i++) |
| send_keysym(0xFFFF); |
| |
| } |
| |
| else |
| send_string(content); |
| |
| // Reset content |
| reset_text_input_target(GuacUI.Client.TEXT_INPUT_PADDING); |
| e.preventDefault(); |
| |
| }, false); |
| |
| // Do not allow event target contents to be selected during input |
| GuacUI.Client.text_input.target.addEventListener("selectstart", function(e) { |
| e.preventDefault(); |
| }, false); |
| |
| /* |
| * Zoom |
| */ |
| |
| GuacUI.Client.auto_fit.onclick = |
| GuacUI.Client.auto_fit.onchange = function() { |
| |
| // If auto-fit enabled, zoom out as far as possible |
| if (GuacUI.Client.auto_fit.checked) |
| GuacUI.Client.setScale(0); |
| |
| // Otherwise, zoom to 1:1 |
| else |
| GuacUI.Client.setScale(1); |
| |
| }; |
| |
| GuacUI.Client.zoom_in.onclick = function() { |
| |
| // Zoom in by 10% |
| var guac = GuacUI.Client.attachedClient; |
| if (guac) |
| GuacUI.Client.setScale(guac.getDisplay().getScale() + 0.1); |
| |
| }; |
| |
| GuacUI.Client.zoom_out.onclick = function() { |
| |
| // Zoom out by 10% |
| var guac = GuacUI.Client.attachedClient; |
| if (guac) |
| GuacUI.Client.setScale(guac.getDisplay().getScale() - 0.1); |
| |
| }; |
| |
| // Prevent default on all touch events |
| document.addEventListener("touchstart", function(e) { |
| |
| // Inspect touch event target to determine whether the touch should |
| // be allowed. |
| if (e.touches.length === 1) { |
| |
| var element = e.target; |
| while (element) { |
| |
| // Allow single-touch events on the menu and text input |
| if (element === GuacUI.Client.menu || element === GuacUI.Client.text_input.container) |
| return; |
| |
| element = element.parentNode; |
| } |
| |
| } |
| |
| e.preventDefault(); |
| |
| }, false); |
| |
| })(); |