/* * 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. */ /** * Main Guacamole UI namespace. * @namespace */ var GuacUI = GuacUI || {}; /** * Creates a new element having the given tagname and CSS class. */ GuacUI.createElement = function(tagname, classname) { var new_element = document.createElement(tagname); if (classname) new_element.className = classname; return new_element; }; /** * Creates a new element having the given tagname, CSS class, and specified * parent element. */ GuacUI.createChildElement = function(parent, tagname, classname) { var element = GuacUI.createElement(tagname, classname); parent.appendChild(element); return element; }; /** * Creates a new row within the given table having a single header cell * with the given title, and a single value cell. The value cell is returned. */ GuacUI.createTabulatedContainer = function(table, title) { // Create elements var row = GuacUI.createChildElement(table, "tr"); var header = GuacUI.createChildElement(row, "th"); var cell = GuacUI.createChildElement(row, "td"); // Set title, return cell header.textContent = title; return cell; }; /** * Adds the given CSS class to the given element. */ GuacUI.addClass = function(element, classname) { // If supported, use native classlist for addClass() if (Node.classlist) element.classList.add(classname); // Otherwise, simply add new class via string manipulation else element.className += " " + classname; }; /** * Removes the given CSS class from the given element. */ GuacUI.removeClass = function(element, classname) { // If supported, use native classlist for removeClass() if (Node.classlist) element.classList.remove(classname); // Otherwise, remove class via string manipulation else { // Filter out classes with given name element.className = element.className.replace(/([^ ]+)[ ]*/g, function(match, testClassname, spaces, offset, string) { // If same class, remove if (testClassname == classname) return ""; // Otherwise, allow return match; } ); } // end if no classlist support }; /** * Opens the connection group having the given ID in a new tab/window. * * @param {String} id The ID of the connection group to open. * @param {String} parameters Any parameters that should be added to the URL, * for sake of authentication. */ GuacUI.openConnectionGroup = function(id, parameters) { GuacUI.openObject("g/" + id, parameters); }; /** * Opens the connection having the given ID in a new tab/window. * * @param {String} id The ID of the connection to open. * @param {String} parameters Any parameters that should be added to the URL, * for sake of authentication. */ GuacUI.openConnection = function(id, parameters) { GuacUI.openObject("c/" + id, parameters); }; /** * Opens the object having the given ID in a new tab/window. The ID must * include the relevant prefix. * * @param {String} id The ID of the object to open, including prefix. * @param {String} parameters Any parameters that should be added to the URL, * for sake of authentication. */ GuacUI.openObject = function(id, parameters) { // Get URL var url = "client.xhtml?id=" + encodeURIComponent(id); // Add parameters, if given if (parameters) url += "&" + parameters; // Attempt to focus existing window var current = window.open(null, id); // If window did not already exist, set up as // Guacamole client if (!current.GuacUI) window.open(url, id); }; /** * Object describing the UI's level of audio support. If the user has request * that audio be disabled, this object will pretend that audio is not * supported. */ GuacUI.Audio = new (function() { var codecs = [ 'audio/ogg; codecs="vorbis"', 'audio/mp4; codecs="mp4a.40.5"', 'audio/mpeg; codecs="mp3"', 'audio/webm; codecs="vorbis"', 'audio/wav; codecs=1' ]; var probably_supported = []; var maybe_supported = []; /** * Array of all supported audio mimetypes, ordered by liklihood of * working. */ this.supported = []; // If sound disabled, we're done now. if (GuacamoleSessionStorage.getItem("disable-sound", false)) return; // Build array of supported audio formats codecs.forEach(function(mimetype) { // Attempt to get audio element for mimetype testing var audio = null; try { audio = new Audio(); } catch (e) { // Skip testing if audio is not available return; } var support_level = audio.canPlayType(mimetype); // Trim semicolon and trailer var semicolon = mimetype.indexOf(";"); if (semicolon != -1) mimetype = mimetype.substring(0, semicolon); // Partition by probably/maybe if (support_level == "probably") probably_supported.push(mimetype); else if (support_level == "maybe") maybe_supported.push(mimetype); }); // Add probably supported types first Array.prototype.push.apply( this.supported, probably_supported); // Prioritize "maybe" supported types second Array.prototype.push.apply( this.supported, maybe_supported); })(); /** * Object describing the UI's level of video support. */ GuacUI.Video = new (function() { var codecs = [ 'video/ogg; codecs="theora, vorbis"', 'video/mp4; codecs="avc1.4D401E, mp4a.40.5"', 'video/webm; codecs="vp8.0, vorbis"' ]; var probably_supported = []; var maybe_supported = []; /** * Array of all supported video mimetypes, ordered by liklihood of * working. */ this.supported = []; // Build array of supported audio formats codecs.forEach(function(mimetype) { var video = document.createElement("video"); var support_level = video.canPlayType(mimetype); // Trim semicolon and trailer var semicolon = mimetype.indexOf(";"); if (semicolon != -1) mimetype = mimetype.substring(0, semicolon); // Partition by probably/maybe if (support_level == "probably") probably_supported.push(mimetype); else if (support_level == "maybe") maybe_supported.push(mimetype); }); // Add probably supported types first Array.prototype.push.apply( this.supported, probably_supported); // Prioritize "maybe" supported types second Array.prototype.push.apply( this.supported, maybe_supported); })(); /** * A connection UI object which can be easily added to a list of connections * for sake of display. */ GuacUI.ListConnection = function(connection) { /** * Reference to this connection. * @private */ var guac_connection = this; /** * The actual connection associated with this connection UI element. */ this.connection = connection; /** * Fired when this connection is clicked. * @event */ this.onclick = null; // Create connection display elements var element = GuacUI.createElement("div", "connection"); var caption = GuacUI.createChildElement(element, "div", "caption"); var protocol = GuacUI.createChildElement(caption, "div", "protocol"); var name = GuacUI.createChildElement(caption, "span", "name"); GuacUI.createChildElement(protocol, "div", "icon " + connection.protocol); element.addEventListener("click", function(e) { // Prevent click from affecting parent e.stopPropagation(); e.preventDefault(); // Fire event if defined if (guac_connection.onclick) guac_connection.onclick(); }, false); // Set name name.textContent = connection.name; // Add active usages (if any) var active_users = connection.currentUsage(); if (active_users > 0) { var usage = GuacUI.createChildElement(caption, "span", "usage"); usage.textContent = "Currently in use by " + active_users + " user(s)"; GuacUI.addClass(element, "in-use"); } /** * Returns the DOM element representing this connection. */ this.getElement = function() { return element; }; }; /** * A paging component. Elements can be added via the addElement() function, * and will only be shown if they are on the current page, set via setPage(). * * Beware that all elements will be added to the given container element, and * all children of the container element will be removed when the page is * changed. */ GuacUI.Pager = function(container) { var guac_pager = this; /** * A container for all pager control buttons. */ var element = GuacUI.createElement("div", "pager"); /** * All displayable elements. */ var elements = []; /** * The number of elements to display per page. */ this.page_capacity = 10; /** * The number of pages to generate a window for. */ this.window_size = 11; /** * The current page, where 0 is the first page. */ this.current_page = 0; /** * The last existing page. */ this.last_page = 0; function update_display() { var i; // Calculate first and last elements of page (where the last element // is actually the first element of the next page) var first_element = guac_pager.current_page * guac_pager.page_capacity; var last_element = Math.min(elements.length, first_element + guac_pager.page_capacity); // Clear contents, add elements container.innerHTML = ""; for (i=first_element; i < last_element; i++) container.appendChild(elements[i]); // Update buttons element.innerHTML = ""; // Create first and prev buttons var first = GuacUI.createChildElement(element, "div", "first-page icon"); var prev = GuacUI.createChildElement(element, "div", "prev-page icon"); // Handle prev/first if (guac_pager.current_page > 0) { first.onclick = function() { guac_pager.setPage(0); }; prev.onclick = function() { guac_pager.setPage(guac_pager.current_page - 1); }; } else { GuacUI.addClass(first, "disabled"); GuacUI.addClass(prev, "disabled"); } // Calculate page jump window start/end var window_start = guac_pager.current_page - (guac_pager.window_size - 1) / 2; var window_end = window_start + guac_pager.window_size - 1; // Shift window as necessary if (window_start < 0) { window_end = Math.min(guac_pager.last_page, window_end - window_start); window_start = 0; } else if (window_end > guac_pager.last_page) { window_start = Math.max(0, window_start - window_end + guac_pager.last_page); window_end = guac_pager.last_page; } // Add ellipsis if window after beginning if (window_start != 0) GuacUI.createChildElement(element, "div", "more-pages").textContent = "..."; // Add page jumps for (i=window_start; i<=window_end; i++) { // Create clickable element containing page number var jump = GuacUI.createChildElement(element, "div", "set-page"); jump.textContent = i+1; // Mark current page if (i == guac_pager.current_page) GuacUI.addClass(jump, "current"); // If not current, add click event else (function(page_number) { jump.onclick = function() { guac_pager.setPage(page_number); }; })(i); } // Add ellipsis if window before end if (window_end != guac_pager.last_page) GuacUI.createChildElement(element, "div", "more-pages").textContent = "..."; // Create next and last buttons var next = GuacUI.createChildElement(element, "div", "next-page icon"); var last = GuacUI.createChildElement(element, "div", "last-page icon"); // Handle next/last if (guac_pager.current_page < guac_pager.last_page) { next.onclick = function() { guac_pager.setPage(guac_pager.current_page + 1); }; last.onclick = function() { guac_pager.setPage(guac_pager.last_page); }; } else { GuacUI.addClass(next, "disabled"); GuacUI.addClass(last, "disabled"); } } /** * Adds the given element to the set of displayable elements. */ this.addElement = function(element) { elements.push(element); guac_pager.last_page = Math.max(0, Math.floor((elements.length - 1) / guac_pager.page_capacity)); }; /** * Sets the current page, where 0 is the first page. */ this.setPage = function(number) { guac_pager.current_page = number; update_display(); }; /** * Returns the element representing the buttons of this pager. */ this.getElement = function() { return element; }; }; /** * Interface object which displays the progress of a download, ultimately * becoming a download link once complete. * * @constructor * @param {String} filename The name the file will have once complete. */ GuacUI.Download = function(filename) { /** * Reference to this GuacUI.Download. * @private */ var guac_download = this; /** * The outer div representing the notification. * @private */ var element = GuacUI.createElement("div", "download notification"); /** * Title bar describing the notification. * @private */ var title = GuacUI.createChildElement(element, "div", "title-bar"); /** * Close button for removing the notification. * @private */ var close_button = GuacUI.createChildElement(title, "div", "close"); close_button.onclick = function() { if (guac_download.onclose) guac_download.onclose(); }; GuacUI.createChildElement(title, "div", "title").textContent = "File Transfer"; GuacUI.createChildElement(element, "div", "caption").textContent = filename + " "; /** * Progress bar and status. * @private */ var progress = GuacUI.createChildElement(element, "div", "progress"); /** * Updates the content of the progress indicator with the given text. * * @param {String} text The text to assign to the progress indicator. */ this.updateProgress = function(text) { progress.textContent = text; }; /** * Updates the content of the dialog to reflect an error condition * represented by the given text. * * @param {String} text A human-readable description of the error. */ this.showError = function(text) { element.removeChild(progress); GuacUI.addClass(element, "error"); var status = GuacUI.createChildElement(element, "div", "status"); status.textContent = text; }; /** * Removes the progress indicator and replaces it with a download button. */ this.complete = function() { element.removeChild(progress); GuacUI.addClass(element, "complete"); var download = GuacUI.createChildElement(element, "button"); download.textContent = "Download"; download.onclick = function() { if (guac_download.ondownload) guac_download.ondownload(); }; }; /** * Returns the element representing this notification. */ this.getElement = function() { return element; }; /** * Called when the close button of this notification is clicked. * @event */ this.onclose = null; /** * Called when the download button of this notification is clicked. * @event */ this.ondownload = null; }; /** * Interface object which displays the progress of a upload. * * @constructor * @param {String} filename The name the file will have once complete. */ GuacUI.Upload = function(filename) { /** * Reference to this GuacUI.Upload. * @private */ var guac_upload = this; /** * The outer div representing the notification. * @private */ var element = GuacUI.createElement("div", "upload notification"); /** * Title bar describing the notification. * @private */ var title = GuacUI.createChildElement(element, "div", "title-bar"); /** * Close button for removing the notification. * @private */ var close_button = GuacUI.createChildElement(title, "div", "close"); close_button.onclick = function() { if (guac_upload.onclose) guac_upload.onclose(); }; GuacUI.createChildElement(title, "div", "title").textContent = "File Transfer"; GuacUI.createChildElement(element, "div", "caption").textContent = filename + " "; /** * Progress bar and status. * @private */ var progress = GuacUI.createChildElement(element, "div", "progress"); /** * The actual moving bar within the progress bar. * @private */ var bar = GuacUI.createChildElement(progress, "div", "bar"); /** * The textual readout of progress. * @private */ var progress_status = GuacUI.createChildElement(progress, "div"); /** * Updates the content of the progress indicator with the given text. * * @param {String} text The text to assign to the progress indicator. * @param {Number} percent The overall percent complete. */ this.updateProgress = function(text, percent) { progress_status.textContent = text; bar.style.width = percent + "%"; }; /** * Updates the content of the dialog to reflect an error condition * represented by the given text. * * @param {String} text A human-readable description of the error. */ this.showError = function(text) { element.removeChild(progress); GuacUI.addClass(element, "error"); var status = GuacUI.createChildElement(element, "div", "status"); status.textContent = text; }; /** * Returns the element representing this notification. */ this.getElement = function() { return element; }; /** * Called when the close button of this notification is clicked. * @event */ this.onclose = null; }; /** * A grouping component. Child elements can be added via the addElement() * function. By default, groups display as collapsed. */ GuacUI.ListGroup = function(caption) { /** * Reference to this group. * @private */ var guac_group = this; /** * Whether this group is empty. * @private */ var empty = true; /** * A container for for the list group itself. */ var element = GuacUI.createElement("div", "group empty"); // Create connection display elements var caption_element = GuacUI.createChildElement(element, "div", "caption"); var caption_icon = GuacUI.createChildElement(caption_element, "div", "icon group"); GuacUI.createChildElement(caption_element, "div", "icon type"); GuacUI.createChildElement(caption_element, "span", "name").textContent = caption; /** * A container for all children of this list group. */ var elements = GuacUI.createChildElement(element, "div", "children"); /** * Whether this group is expanded. * * @type Boolean */ this.expanded = false; /** * Fired when this group is clicked. * @event */ this.onclick = null; /** * Returns the element representing this notification. */ this.getElement = function() { return element; }; /** * Adds an element as a child of this group. */ this.addElement = function(child) { // Mark as non-empty if (empty) { GuacUI.removeClass(element, "empty"); empty = false; } elements.appendChild(child); }; /** * Expands the list group, revealing all children of the group. This * functionality requires supporting CSS. */ this.expand = function() { GuacUI.addClass(element, "expanded"); guac_group.expanded = true; }; /** * Collapses the list group, hiding all children of the group. This * functionality requires supporting CSS. */ this.collapse = function() { GuacUI.removeClass(element, "expanded"); guac_group.expanded = false; }; // Toggle when icon is clicked caption_icon.addEventListener("click", function(e) { // Prevent click from affecting parent e.stopPropagation(); e.preventDefault(); if (guac_group.expanded) guac_group.collapse(); else guac_group.expand(); }, false); // Fire event when any other part is clicked element.addEventListener("click", function(e) { // Prevent click from affecting parent e.stopPropagation(); e.preventDefault(); // Fire event if defined if (guac_group.onclick) guac_group.onclick(); }, false); } /** * Component which displays a paginated tree view of all groups and their * connections. * * @constructor * @param {GuacamoleService.ConnectionGroup} root_group The group to display * within the view. * @param {Number} flags Any flags (such as MULTISELECT or SHOW_CONNECTIONS) * for modifying the behavior of this group view. * @param {Function} group_filter Function which returns true if the given * group should be displayed and false otherwise. * @param {Function} connection_filter Function which returns true if the given * connection should be displayed and false * otherwise. */ GuacUI.GroupView = function(root_group, flags, group_filter, connection_filter) { /** * Reference to this GroupView. * @private */ var group_view = this; // Group view components var element = GuacUI.createElement("div", "group-view"); var list = GuacUI.createChildElement(element, "div", "list"); /** * Whether multiselect is enabled. */ var multiselect = flags & GuacUI.GroupView.MULTISELECT; /** * Whether connections should be included in the view. */ var show_connections = flags & GuacUI.GroupView.SHOW_CONNECTIONS; /** * Whether the root group should be included in the view. */ var show_root = flags & GuacUI.GroupView.SHOW_ROOT_GROUP; /** * Set of all group checkboxes, indexed by ID. Only applicable when * multiselect is enabled. * @private */ var group_checkboxes = {}; /** * Set of all connection checkboxes, indexed by ID. Only applicable when * multiselect is enabled. * @private */ var connection_checkboxes = {}; /** * Set of all list groups, indexed by associated group ID. * @private */ var list_groups = {}; /** * Set of all connection groups, indexed by ID. */ this.groups = {}; /** * Set of all connections, indexed by ID. */ this.connections = {}; /** * Fired when a connection is clicked. * * @event * @param {GuacamolService.Connection} connection The connection which was * clicked. */ this.onconnectionclick = null; /** * Fired when a connection group is clicked. * * @event * @param {GuacamolService.ConnectionGroup} group The connection group which * was clicked. */ this.ongroupclick = null; /** * Fired when a connection's selected status changes. * * @event * @param {GuacamolService.Connection} connection The connection whose * status changed. * @param {Boolean} selected The new status of the connection. */ this.onconnectionchange = null; /** * Fired when a connection group's selected status changes. * * @event * @param {GuacamolService.ConnectionGroup} group The connection group whose * status changed. * @param {Boolean} selected The new status of the connection group. */ this.ongroupchange = null; /** * Returns the element representing this group view. */ this.getElement = function() { return element; }; /** * Sets whether the group with the given ID can be selected. This function * only has an effect when multiselect is enabled. * * @param {String} id The ID of the group to alter. * @param {Boolean} value Whether the group should be selected. */ this.setGroupEnabled = function(id, value) { var checkbox = group_checkboxes[id]; if (!checkbox) return; // If enabled, show checkbox, allow select if (value) { checkbox.style.visibility = ""; checkbox.disabled = false; } // Otherwise, hide checkbox else { checkbox.style.visibility = "hidden"; checkbox.disabled = true; } }; /** * Sets whether the connection with the given ID can be selected. This * function only has an effect when multiselect is enabled. * * @param {String} id The ID of the connection to alter. * @param {Boolean} value Whether the connection can be selected. */ this.setConnectionEnabled = function(id, value) { var checkbox = connection_checkboxes[id]; if (!checkbox) return; // If enabled, show checkbox, allow select if (value) { checkbox.style.visibility = ""; checkbox.disabled = false; } // Otherwise, hide checkbox else { checkbox.style.visibility = "hidden"; checkbox.disabled = true; } }; /** * Sets the current value of the group with the given ID. This function * only has an effect when multiselect is enabled. * * @param {String} id The ID of the group to change. * @param {Boolean} value Whether the group should be selected. */ this.setGroupValue = function(id, value) { var checkbox = group_checkboxes[id]; if (!checkbox) return; checkbox.checked = value; }; /** * Sets the current value of the connection with the given ID. This function * only has an effect when multiselect is enabled. * * @param {String} id The ID of the connection to change. * @param {Boolean} value Whether the connection should be selected. */ this.setConnectionValue = function(id, value) { var checkbox = connection_checkboxes[id]; if (!checkbox) return; checkbox.checked = value; }; /** * Expands the given group and all parent groups all the way up to root. * * @param {GuacamoleService.ConnectionGroup} group The group that should * be expanded. */ this.expand = function(group) { // Skip current group - only need to expand parents group = group.parent; // For each group all the way to root while (group !== null) { // If list group exists, expand it var list_group = list_groups[group.id]; if (list_group) list_group.expand(); group = group.parent; } } // Create pager for contents var pager = new GuacUI.Pager(list); pager.page_capacity = 20; /** * Adds the contents of the given group via the given appendChild() * function, but not the given group itself. * * @param {GuacamoleService.ConnectionGroup} group The group whose contents * should be added. * @param {Function} appendChild A function which, given an element, will * add that element the the display as * desired. */ function addGroupContents(group, appendChild) { var i; // Add all contained connections if (show_connections) { for (i=0; i