GUACAMOLE-773: Migrate to NPM for AngularJS portion of webapp build.

This commit is contained in:
Michael Jumper
2021-04-05 14:47:04 -07:00
parent 71948a54ca
commit 1ef61687d8
426 changed files with 7086 additions and 234 deletions

View File

@@ -0,0 +1,4 @@
*~
node_modules
dist
generated

View File

@@ -0,0 +1,34 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* @fileoverview External APIs referenced by the source of the Guacamole webapp
* and its dependencies.
* @externs
*/
// guacamole-common-js
const Guacamole = {};
// Web Storage API
const localStorage = {};
// matchMedia() function of Window object
const matchMedia = function matchMedia(str) {}

View File

@@ -0,0 +1,105 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
const angularFilesort = require('gulp-angular-filesort');
const cleanCss = require('gulp-clean-css');
const concat = require('gulp-concat');
const del = require('del');
const gulp = require('gulp');
const ngHtml2Js = require("gulp-ng-html2js");
const sourcemaps = require('gulp-sourcemaps');
const webpack = require('webpack-stream');
// Clean build files
gulp.task('clean', (callback) => del([
'dist',
'generated'
], callback));
// Build monolithic, minified CSS source
gulp.task('build-css',
() => gulp.src([
'node_modules/@simonwep/pickr/dist/themes/monolith.min.css',
'src/app/**/*.css'
])
.pipe(sourcemaps.init())
.pipe(concat('guacamole.min.css'))
.pipe(cleanCss())
.pipe(sourcemaps.write('./'))
.pipe(gulp.dest('dist'))
);
// Pre-cache AngularJS templates
gulp.task('build-template-js',
() => gulp.src('src/app/**/*.html')
.pipe(ngHtml2Js({
moduleName: 'templates-main',
prefix: 'app/'
}))
.pipe(concat('templates.js'))
.pipe(gulp.dest('generated'))
);
// Build monolithic combined JavaScript source containing all pre-cached
// templates and all AngularJS module declarations in the proper order
gulp.task('build-combined-js',
() => gulp.src([
'src/app/**/*.js',
'generated/templates.js'
])
.pipe(angularFilesort())
.pipe(sourcemaps.init())
.pipe(concat('guacamole.js'))
.pipe(sourcemaps.write('./'))
.pipe(gulp.dest('generated'))
);
// Process monolithic JavaScript source through WebPack to produce a bundle
// that contains all required dependencies
gulp.task('build-webpack-bundle',
() => gulp.src('generated/guacamole.js')
.pipe(webpack(require('./webpack.config.js')))
.pipe(gulp.dest('dist'))
);
// Build all JavaScript for the entire application
gulp.task('build-js', gulp.series(
'build-template-js',
'build-combined-js',
'build-webpack-bundle'
));
// Copy plain, static contents of application
gulp.task('copy-static',
() => gulp.src([
'src/relocateParameters.js',
'src/index.html',
'src/fonts/**/*',
'src/images/**/*',
'src/layouts/**/*',
'src/translations/**/*'
], { base: './src' })
.pipe(gulp.dest('dist'))
);
gulp.task('default', gulp.series(
'clean',
gulp.parallel('build-css', 'build-js', 'copy-static')
));

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,36 @@
{
"private": true,
"scripts": {
"build": "gulp"
},
"dependencies": {
"@simonwep/pickr": "1.2.6",
"angular": "1.6.9",
"angular-route": "1.6.9",
"angular-touch": "1.6.9",
"angular-translate": "2.16.0",
"angular-translate-interpolation-messageformat": "2.16.0",
"angular-translate-loader-static-files": "2.16.0",
"blob-polyfill": "1.0.20150320",
"datalist-polyfill": "1.14.0",
"file-saver": "1.3.3",
"jquery": "3.3.1",
"jstz": "1.0.10",
"lodash": "4.17.10",
"messageformat": "1.0.2"
},
"devDependencies": {
"closure-webpack-plugin": "^2.5.0",
"del": "^6.0.0",
"google-closure-compiler": "^20210302.0.0",
"gulp": "^4.0.2",
"gulp-angular-filesort": "^1.2.1",
"gulp-clean-css": "^4.3.0",
"gulp-concat": "^2.6.1",
"gulp-ng-html2js": "^0.2.3",
"gulp-sourcemaps": "^3.0.0",
"source-map-loader": "^1.1.3",
"webpack": "^4.46.0",
"webpack-stream": "^6.1.2"
}
}

View File

@@ -0,0 +1,26 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* The module for authentication and management of tokens.
*/
angular.module('auth', [
'rest',
'storage'
]);

View File

@@ -0,0 +1,398 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A service for authenticating a user against the REST API.
*
* This service broadcasts two events on $rootScope depending on the result of
* authentication operations: 'guacLogin' if authentication was successful and
* a new token was created, and 'guacLogout' if an existing token is being
* destroyed or replaced. Both events will be passed the related token as their
* sole parameter.
*
* If a login attempt results in an existing token being replaced, 'guacLogout'
* will be broadcast first for the token being replaced, followed by
* 'guacLogin' for the new token.
*
* Failed logins may also result in guacInsufficientCredentials or
* guacInvalidCredentials events, if the provided credentials were rejected for
* being insufficient or invalid respectively. Both events will be provided
* the set of parameters originally given to authenticate() and the error that
* rejected the credentials. The Error object provided will contain set of
* expected credentials returned by the REST endpoint. This set of credentials
* will be in the form of a Field array.
*/
angular.module('auth').factory('authenticationService', ['$injector',
function authenticationService($injector) {
// Required types
var AuthenticationResult = $injector.get('AuthenticationResult');
var Error = $injector.get('Error');
// Required services
var $rootScope = $injector.get('$rootScope');
var localStorageService = $injector.get('localStorageService');
var requestService = $injector.get('requestService');
var service = {};
/**
* The most recent authentication result, or null if no authentication
* result is cached.
*
* @type AuthenticationResult
*/
var cachedResult = null;
/**
* The unique identifier of the local storage key which stores the result
* of the last authentication attempt.
*
* @type String
*/
var AUTH_STORAGE_KEY = 'GUAC_AUTH';
/**
* Retrieves the last successful authentication result. If the user has not
* yet authenticated, the user has logged out, or the last authentication
* attempt failed, null is returned.
*
* @returns {AuthenticationResult}
* The last successful authentication result, or null if the user is not
* currently authenticated.
*/
var getAuthenticationResult = function getAuthenticationResult() {
// Use cached result, if any
if (cachedResult)
return cachedResult;
// Return explicit null if no auth data is currently stored
var data = localStorageService.getItem(AUTH_STORAGE_KEY);
if (!data)
return null;
// Update cache and return retrieved auth result
return (cachedResult = new AuthenticationResult(data));
};
/**
* Stores the given authentication result for future retrieval. The given
* result MUST be the result of the most recent authentication attempt.
*
* @param {AuthenticationResult} data
* The last successful authentication result, or null if the last
* authentication attempt failed.
*/
var setAuthenticationResult = function setAuthenticationResult(data) {
// Clear the currently-stored result if the last attempt failed
if (!data) {
cachedResult = null;
localStorageService.removeItem(AUTH_STORAGE_KEY);
}
// Otherwise store the authentication attempt directly
else {
// Always store in cache
cachedResult = data;
// Persist result past tab/window closure ONLY if not anonymous
if (data.username !== AuthenticationResult.ANONYMOUS_USERNAME)
localStorageService.setItem(AUTH_STORAGE_KEY, data);
}
};
/**
* Clears the stored authentication result, if any. If no authentication
* result is currently stored, this function has no effect.
*/
var clearAuthenticationResult = function clearAuthenticationResult() {
setAuthenticationResult(null);
};
/**
* Makes a request to authenticate a user using the token REST API endpoint
* and given arbitrary parameters, returning a promise that succeeds only
* if the authentication operation was successful. The resulting
* authentication data can be retrieved later via getCurrentToken() or
* getCurrentUsername().
*
* The provided parameters can be virtually any object, as each property
* will be sent as an HTTP parameter in the authentication request.
* Standard parameters include "username" for the user's username,
* "password" for the user's associated password, and "token" for the
* auth token to check/update.
*
* If a token is provided, it will be reused if possible.
*
* @param {Object} parameters
* Arbitrary parameters to authenticate with.
*
* @returns {Promise}
* A promise which succeeds only if the login operation was successful.
*/
service.authenticate = function authenticate(parameters) {
// Attempt authentication
return requestService({
method: 'POST',
url: 'api/tokens',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: $.param(parameters)
})
// If authentication succeeds, handle received auth data
.then(function authenticationSuccessful(data) {
var currentToken = service.getCurrentToken();
// If a new token was received, ensure the old token is invalidated,
// if any, and notify listeners of the new token
if (data.authToken !== currentToken) {
// If an old token existed, request that the token be revoked
if (currentToken) {
service.logout().catch(angular.noop)
}
// Notify of login and new token
setAuthenticationResult(new AuthenticationResult(data));
$rootScope.$broadcast('guacLogin', data.authToken);
}
// Update cached authentication result, even if the token remains
// the same
else
setAuthenticationResult(new AuthenticationResult(data));
// Authentication was successful
return data;
})
// If authentication fails, propogate failure to returned promise
['catch'](requestService.createErrorCallback(function authenticationFailed(error) {
// Request credentials if provided credentials were invalid
if (error.type === Error.Type.INVALID_CREDENTIALS)
$rootScope.$broadcast('guacInvalidCredentials', parameters, error);
// Request more credentials if provided credentials were not enough
else if (error.type === Error.Type.INSUFFICIENT_CREDENTIALS)
$rootScope.$broadcast('guacInsufficientCredentials', parameters, error);
// Abort rendering of page if an internal error occurs
else if (error.type === Error.Type.INTERNAL_ERROR)
$rootScope.$broadcast('guacFatalPageError', error);
// Authentication failed
throw error;
}));
};
/**
* Makes a request to update the current auth token, if any, using the
* token REST API endpoint. If the optional parameters object is provided,
* its properties will be included as parameters in the update request.
* This function returns a promise that succeeds only if the authentication
* operation was successful. The resulting authentication data can be
* retrieved later via getCurrentToken() or getCurrentUsername().
*
* If there is no current auth token, this function behaves identically to
* authenticate(), and makes a general authentication request.
*
* @param {Object} [parameters]
* Arbitrary parameters to authenticate with, if any.
*
* @returns {Promise}
* A promise which succeeds only if the login operation was successful.
*/
service.updateCurrentToken = function updateCurrentToken(parameters) {
// HTTP parameters for the authentication request
var httpParameters = {};
// Add token parameter if current token is known
var token = service.getCurrentToken();
if (token)
httpParameters.token = service.getCurrentToken();
// Add any additional parameters
if (parameters)
angular.extend(httpParameters, parameters);
// Make the request
return service.authenticate(httpParameters);
};
/**
* Makes a request to authenticate a user using the token REST API endpoint
* with a username and password, ignoring any currently-stored token,
* returning a promise that succeeds only if the login operation was
* successful. The resulting authentication data can be retrieved later
* via getCurrentToken() or getCurrentUsername().
*
* @param {String} username
* The username to log in with.
*
* @param {String} password
* The password to log in with.
*
* @returns {Promise}
* A promise which succeeds only if the login operation was successful.
*/
service.login = function login(username, password) {
return service.authenticate({
username: username,
password: password
});
};
/**
* Makes a request to logout a user using the login REST API endpoint,
* returning a promise succeeds only if the logout operation was
* successful.
*
* @returns {Promise}
* A promise which succeeds only if the logout operation was
* successful.
*/
service.logout = function logout() {
// Clear authentication data
var token = service.getCurrentToken();
clearAuthenticationResult();
// Notify listeners that a token is being destroyed
$rootScope.$broadcast('guacLogout', token);
// Delete old token
return requestService({
method: 'DELETE',
url: 'api/tokens/' + token
});
};
/**
* Returns whether the current user has authenticated anonymously. An
* anonymous user is denoted by the identifier reserved by the Guacamole
* extension API for anonymous users (the empty string).
*
* @returns {Boolean}
* true if the current user has authenticated anonymously, false
* otherwise.
*/
service.isAnonymous = function isAnonymous() {
return service.getCurrentUsername() === '';
};
/**
* Returns the username of the current user. If the current user is not
* logged in, this value may not be valid.
*
* @returns {String}
* The username of the current user, or null if no authentication data
* is present.
*/
service.getCurrentUsername = function getCurrentUsername() {
// Return username, if available
var authData = getAuthenticationResult();
if (authData)
return authData.username;
// No auth data present
return null;
};
/**
* Returns the auth token associated with the current user. If the current
* user is not logged in, this token may not be valid.
*
* @returns {String}
* The auth token associated with the current user, or null if no
* authentication data is present.
*/
service.getCurrentToken = function getCurrentToken() {
// Return auth token, if available
var authData = getAuthenticationResult();
if (authData)
return authData.authToken;
// No auth data present
return null;
};
/**
* Returns the identifier of the data source that authenticated the current
* user. If the current user is not logged in, this value may not be valid.
*
* @returns {String}
* The identifier of the data source that authenticated the current
* user, or null if no authentication data is present.
*/
service.getDataSource = function getDataSource() {
// Return data source, if available
var authData = getAuthenticationResult();
if (authData)
return authData.dataSource;
// No auth data present
return null;
};
/**
* Returns the identifiers of all data sources available to the current
* user. If the current user is not logged in, this value may not be valid.
*
* @returns {String[]}
* The identifiers of all data sources availble to the current user,
* or an empty array if no authentication data is present.
*/
service.getAvailableDataSources = function getAvailableDataSources() {
// Return data sources, if available
var authData = getAuthenticationResult();
if (authData)
return authData.availableDataSources;
// No auth data present
return [];
};
return service;
}]);

View File

@@ -0,0 +1,81 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Service which defines the AuthenticationResult class.
*/
angular.module('auth').factory('AuthenticationResult', [function defineAuthenticationResult() {
/**
* The object returned by REST API calls when representing the successful
* result of an authentication attempt.
*
* @constructor
* @param {AuthenticationResult|Object} [template={}]
* The object whose properties should be copied within the new
* AuthenticationResult.
*/
var AuthenticationResult = function AuthenticationResult(template) {
// Use empty object by default
template = template || {};
/**
* The unique token generated for the user that authenticated.
*
* @type String
*/
this.authToken = template.authToken;
/**
* The name which uniquely identifies the user that authenticated.
*
* @type String
*/
this.username = template.username;
/**
* The unique identifier of the data source which authenticated the
* user.
*
* @type String
*/
this.dataSource = template.dataSource;
/**
* The identifiers of all data sources available to the user that
* authenticated.
*
* @type String[]
*/
this.availableDataSources = template.availableDataSources;
};
/**
* The username reserved by the Guacamole extension API for users which have
* authenticated anonymously.
*
* @type String
*/
AuthenticationResult.ANONYMOUS_USERNAME = '';
return AuthenticationResult;
}]);

View File

@@ -0,0 +1,34 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* The module for code used to connect to a connection or balancing group.
*/
angular.module('client', [
'auth',
'clipboard',
'element',
'history',
'navigation',
'notification',
'osk',
'rest',
'textInput',
'touch'
]);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,485 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive for the guacamole client.
*/
angular.module('client').directive('guacClient', [function guacClient() {
return {
// Element only
restrict: 'E',
replace: true,
scope: {
/**
* The client to display within this guacClient directive.
*
* @type ManagedClient
*/
client : '='
},
templateUrl: 'app/client/templates/guacClient.html',
controller: ['$scope', '$injector', '$element', function guacClientController($scope, $injector, $element) {
// Required types
var ManagedClient = $injector.get('ManagedClient');
// Required services
var $window = $injector.get('$window');
/**
* Whether the local, hardware mouse cursor is in use.
*
* @type Boolean
*/
var localCursor = false;
/**
* The current Guacamole client instance.
*
* @type Guacamole.Client
*/
var client = null;
/**
* The display of the current Guacamole client instance.
*
* @type Guacamole.Display
*/
var display = null;
/**
* The element associated with the display of the current
* Guacamole client instance.
*
* @type Element
*/
var displayElement = null;
/**
* The element which must contain the Guacamole display element.
*
* @type Element
*/
var displayContainer = $element.find('.display')[0];
/**
* The main containing element for the entire directive.
*
* @type Element
*/
var main = $element[0];
/**
* The element which functions as a detector for size changes.
*
* @type Element
*/
var resizeSensor = $element.find('.resize-sensor')[0];
/**
* Guacamole mouse event object, wrapped around the main client
* display.
*
* @type Guacamole.Mouse
*/
var mouse = new Guacamole.Mouse(displayContainer);
/**
* Guacamole absolute mouse emulation object, wrapped around the
* main client display.
*
* @type Guacamole.Mouse.Touchscreen
*/
var touchScreen = new Guacamole.Mouse.Touchscreen(displayContainer);
/**
* Guacamole relative mouse emulation object, wrapped around the
* main client display.
*
* @type Guacamole.Mouse.Touchpad
*/
var touchPad = new Guacamole.Mouse.Touchpad(displayContainer);
/**
* Guacamole touch event handling object, wrapped around the main
* client dislay.
*
* @type Guacamole.Touch
*/
var touch = new Guacamole.Touch(displayContainer);
/**
* Updates the scale of the attached Guacamole.Client based on current window
* size and "auto-fit" setting.
*/
var updateDisplayScale = function updateDisplayScale() {
if (!display) return;
// Calculate scale to fit screen
$scope.client.clientProperties.minScale = Math.min(
main.offsetWidth / Math.max(display.getWidth(), 1),
main.offsetHeight / Math.max(display.getHeight(), 1)
);
// Calculate appropriate maximum zoom level
$scope.client.clientProperties.maxScale = Math.max($scope.client.clientProperties.minScale, 3);
// Clamp zoom level, maintain auto-fit
if (display.getScale() < $scope.client.clientProperties.minScale || $scope.client.clientProperties.autoFit)
$scope.client.clientProperties.scale = $scope.client.clientProperties.minScale;
else if (display.getScale() > $scope.client.clientProperties.maxScale)
$scope.client.clientProperties.scale = $scope.client.clientProperties.maxScale;
};
/**
* Scrolls the client view such that the mouse cursor is visible.
*
* @param {Guacamole.Mouse.State} mouseState The current mouse
* state.
*/
var scrollToMouse = function scrollToMouse(mouseState) {
// Determine mouse position within view
var mouse_view_x = mouseState.x + displayContainer.offsetLeft - main.scrollLeft;
var mouse_view_y = mouseState.y + displayContainer.offsetTop - main.scrollTop;
// Determine viewport dimensions
var view_width = main.offsetWidth;
var view_height = 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.
main.scrollLeft += scroll_amount_x;
main.scrollTop += scroll_amount_y;
};
/**
* Handles a mouse event originating from the user's actual mouse.
* This differs from handleEmulatedMouseEvent() in that the
* software mouse cursor must be shown only if the user's browser
* does not support explicitly setting the hardware mouse cursor.
*
* @param {Guacamole.Mouse.MouseEvent} event
* The mouse event to handle.
*/
var handleMouseEvent = function handleMouseEvent(event) {
// Do not attempt to handle mouse state changes if the client
// or display are not yet available
if (!client || !display)
return;
event.stopPropagation();
event.preventDefault();
// Send mouse state, show cursor if necessary
display.showCursor(!localCursor);
client.sendMouseState(event.state, true);
};
/**
* Handles a mouse event originating from one of Guacamole's mouse
* emulation objects. This differs from handleMouseState() in that
* the software mouse cursor must always be shown (as the emulated
* mouse device will not have its own cursor).
*
* @param {Guacamole.Mouse.MouseEvent} event
* The mouse event to handle.
*/
var handleEmulatedMouseEvent = function handleEmulatedMouseEvent(event) {
// Do not attempt to handle mouse state changes if the client
// or display are not yet available
if (!client || !display)
return;
event.stopPropagation();
event.preventDefault();
// Ensure software cursor is shown
display.showCursor(true);
// Send mouse state, ensure cursor is visible
scrollToMouse(event.state);
client.sendMouseState(event.state, true);
};
/**
* Handles a touch event originating from the user's device.
*
* @param {Guacamole.Touch.Event} touchEvent
* The touch event.
*/
var handleTouchEvent = function handleTouchEvent(event) {
// Do not attempt to handle touch state changes if the client
// or display are not yet available
if (!client || !display)
return;
event.preventDefault();
// Send touch state, hiding local cursor
display.showCursor(false);
client.sendTouchState(event.state, true);
};
// Attach any given managed client
$scope.$watch('client', function attachManagedClient(managedClient) {
// Remove any existing display
displayContainer.innerHTML = "";
// Only proceed if a client is given
if (!managedClient)
return;
// Get Guacamole client instance
client = managedClient.client;
// Attach possibly new display
display = client.getDisplay();
display.scale($scope.client.clientProperties.scale);
// Add display element
displayElement = display.getElement();
displayContainer.appendChild(displayElement);
// Do nothing when the display element is clicked on
display.getElement().onclick = function(e) {
e.preventDefault();
return false;
};
// Size of newly-attached client may be different
$scope.mainElementResized();
});
// Update actual view scrollLeft when scroll properties change
$scope.$watch('client.clientProperties.scrollLeft', function scrollLeftChanged(scrollLeft) {
main.scrollLeft = scrollLeft;
$scope.client.clientProperties.scrollLeft = main.scrollLeft;
});
// Update actual view scrollTop when scroll properties change
$scope.$watch('client.clientProperties.scrollTop', function scrollTopChanged(scrollTop) {
main.scrollTop = scrollTop;
$scope.client.clientProperties.scrollTop = main.scrollTop;
});
// Update scale when display is resized
$scope.$watch('client.managedDisplay.size', function setDisplaySize() {
$scope.$evalAsync(updateDisplayScale);
});
// Keep local cursor up-to-date
$scope.$watch('client.managedDisplay.cursor', function setCursor(cursor) {
if (cursor)
localCursor = mouse.setCursor(cursor.canvas, cursor.x, cursor.y);
});
// Update touch event handling depending on remote multi-touch
// support and mouse emulation mode
$scope.$watchGroup([
'client.multiTouchSupport',
'client.clientProperties.emulateAbsoluteMouse'
], function touchBehaviorChanged(emulateAbsoluteMouse) {
// Clear existing event handling
touch.offEach(['touchstart', 'touchmove', 'touchend'], handleTouchEvent);
touchScreen.offEach(['mousedown', 'mousemove', 'mouseup'], handleEmulatedMouseEvent);
touchPad.offEach(['mousedown', 'mousemove', 'mouseup'], handleEmulatedMouseEvent);
// Directly forward local touch events
if ($scope.client.multiTouchSupport)
touch.onEach(['touchstart', 'touchmove', 'touchend'], handleTouchEvent);
// Switch to touchscreen if mouse emulation is required and
// absolute mouse emulation is preferred
else if ($scope.client.clientProperties.emulateAbsoluteMouse)
touchScreen.onEach(['mousedown', 'mousemove', 'mouseup'], handleEmulatedMouseEvent);
// Use touchpad for mouse emulation if absolute mouse emulation
// is not preferred
else
touchPad.onEach(['mousedown', 'mousemove', 'mouseup'], handleEmulatedMouseEvent);
});
// Adjust scale if modified externally
$scope.$watch('client.clientProperties.scale', function changeScale(scale) {
// Fix scale within limits
scale = Math.max(scale, $scope.client.clientProperties.minScale);
scale = Math.min(scale, $scope.client.clientProperties.maxScale);
// If at minimum zoom level, hide scroll bars
if (scale === $scope.client.clientProperties.minScale)
main.style.overflow = "hidden";
// If not at minimum zoom level, show scroll bars
else
main.style.overflow = "auto";
// Apply scale if client attached
if (display)
display.scale(scale);
if (scale !== $scope.client.clientProperties.scale)
$scope.client.clientProperties.scale = scale;
});
// If autofit is set, the scale should be set to the minimum scale, filling the screen
$scope.$watch('client.clientProperties.autoFit', function changeAutoFit(autoFit) {
if(autoFit)
$scope.client.clientProperties.scale = $scope.client.clientProperties.minScale;
});
// If the element is resized, attempt to resize client
$scope.mainElementResized = function mainElementResized() {
// Send new display size, if changed
if (client && display) {
var pixelDensity = $window.devicePixelRatio || 1;
var width = main.offsetWidth * pixelDensity;
var height = main.offsetHeight * pixelDensity;
if (display.getWidth() !== width || display.getHeight() !== height)
client.sendSize(width, height);
}
$scope.$evalAsync(updateDisplayScale);
};
// Ensure focus is regained via mousedown before forwarding event
mouse.on('mousedown', document.body.focus.bind(document.body));
// Forward all mouse events
mouse.onEach(['mousedown', 'mousemove', 'mouseup'], handleMouseEvent);
// Hide software cursor when mouse leaves display
mouse.on('mouseout', function() {
if (!display) return;
display.showCursor(false);
});
// Update remote clipboard if local clipboard changes
$scope.$on('guacClipboard', function onClipboard(event, data) {
if (client) {
ManagedClient.setClipboard($scope.client, data);
$scope.client.clipboardData = data;
}
});
// Translate local keydown events to remote keydown events if keyboard is enabled
$scope.$on('guacKeydown', function keydownListener(event, keysym, keyboard) {
if ($scope.client.clientProperties.keyboardEnabled && !event.defaultPrevented) {
client.sendKeyEvent(1, keysym);
event.preventDefault();
}
});
// Translate local keyup events to remote keyup events if keyboard is enabled
$scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) {
if ($scope.client.clientProperties.keyboardEnabled && !event.defaultPrevented) {
client.sendKeyEvent(0, keysym);
event.preventDefault();
}
});
// Universally handle all synthetic keydown events
$scope.$on('guacSyntheticKeydown', function syntheticKeydownListener(event, keysym) {
client.sendKeyEvent(1, keysym);
});
// Universally handle all synthetic keyup events
$scope.$on('guacSyntheticKeyup', function syntheticKeyupListener(event, keysym) {
client.sendKeyEvent(0, keysym);
});
/**
* Ignores the given event.
*
* @param {Event} e The event to ignore.
*/
function ignoreEvent(e) {
e.preventDefault();
e.stopPropagation();
}
// Handle and ignore dragenter/dragover
displayContainer.addEventListener("dragenter", ignoreEvent, false);
displayContainer.addEventListener("dragover", ignoreEvent, false);
// File drop event handler
displayContainer.addEventListener("drop", function(e) {
e.preventDefault();
e.stopPropagation();
// Ignore file drops if no attached client
if (!$scope.client)
return;
// Upload each file
var files = e.dataTransfer.files;
for (var i=0; i<files.length; i++)
ManagedClient.uploadFile($scope.client, files[i]);
}, false);
/*
* END CLIENT DIRECTIVE
*/
}]
};
}]);

View File

@@ -0,0 +1,170 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A toolbar/panel which displays a list of active Guacamole connections. The
* panel is fixed to the bottom-right corner of its container and can be
* manually hidden/exposed by the user.
*/
angular.module('client').directive('guacClientPanel', ['$injector', function guacClientPanel($injector) {
// Required services
var guacClientManager = $injector.get('guacClientManager');
var sessionStorageFactory = $injector.get('sessionStorageFactory');
// Required types
var ManagedClientState = $injector.get('ManagedClientState');
/**
* Getter/setter for the boolean flag controlling whether the client panel
* is currently hidden. This flag is maintained in session-local storage to
* allow the state of the panel to persist despite navigation within the
* same tab. When hidden, the panel will be collapsed against the right
* side of the container. By default, the panel is visible.
*
* @type Function
*/
var panelHidden = sessionStorageFactory.create(false);
return {
// Element only
restrict: 'E',
replace: true,
scope: {
/**
* The ManagedClient instances associated with the active
* connections to be displayed within this panel.
*
* @type ManagedClient[]|Object.<String, ManagedClient>
*/
clients : '='
},
templateUrl: 'app/client/templates/guacClientPanel.html',
controller: ['$scope', '$element', function guacClientPanelController($scope, $element) {
/**
* The DOM element containing the scrollable portion of the client
* panel.
*
* @type Element
*/
var scrollableArea = $element.find('.client-panel-connection-list')[0];
/**
* On-scope reference to session-local storage of the flag
* controlling whether then panel is hidden.
*/
$scope.panelHidden = panelHidden;
/**
* Returns whether this panel currently has any clients associated
* with it.
*
* @return {Boolean}
* true if at least one client is associated with this panel,
* false otherwise.
*/
$scope.hasClients = function hasClients() {
return !!_.find($scope.clients, $scope.isManaged);
};
/**
* Returns whether the status of the given client has changed in a
* way that requires the user's attention. This may be due to an
* error, or due to a server-initiated disconnect.
*
* @param {ManagedClient} client
* The client to test.
*
* @returns {Boolean}
* true if the given client requires the user's attention,
* false otherwise.
*/
$scope.hasStatusUpdate = function hasStatusUpdate(client) {
// Test whether the client has encountered an error
switch (client.clientState.connectionState) {
case ManagedClientState.ConnectionState.CONNECTION_ERROR:
case ManagedClientState.ConnectionState.TUNNEL_ERROR:
case ManagedClientState.ConnectionState.DISCONNECTED:
return true;
}
return false;
};
/**
* Returns whether the given client is currently being managed by
* the guacClientManager service.
*
* @param {ManagedClient} client
* The client to test.
*
* @returns {Boolean}
* true if the given client is being managed by the
* guacClientManager service, false otherwise.
*/
$scope.isManaged = function isManaged(client) {
return !!guacClientManager.getManagedClients()[client.id];
};
/**
* Initiates an orderly disconnect of the given client. The client
* is removed from management such that attempting to connect to
* the same connection will result in a new connection being
* established, rather than displaying a notification that the
* connection has ended.
*
* @param {type} client
* @returns {undefined}
*/
$scope.disconnect = function disconnect(client) {
client.client.disconnect();
guacClientManager.removeManagedClient(client.id);
};
/**
* Toggles whether the client panel is currently hidden.
*/
$scope.togglePanel = function togglePanel() {
panelHidden(!panelHidden());
};
// Override vertical scrolling, scrolling horizontally instead
scrollableArea.addEventListener('wheel', function reorientVerticalScroll(e) {
var deltaMultiplier = {
/* DOM_DELTA_PIXEL */ 0x00: 1,
/* DOM_DELTA_LINE */ 0x01: 15,
/* DOM_DELTA_PAGE */ 0x02: scrollableArea.offsetWidth
};
if (e.deltaY) {
this.scrollLeft += e.deltaY * (deltaMultiplier[e.deltaMode] || deltaMultiplier(0x01));
e.preventDefault();
}
});
}]
};
}]);

View File

@@ -0,0 +1,289 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive which displays the contents of a filesystem received through the
* Guacamole client.
*/
angular.module('client').directive('guacFileBrowser', [function guacFileBrowser() {
return {
restrict: 'E',
replace: true,
scope: {
/**
* The client whose file transfers should be managed by this
* directive.
*
* @type ManagedClient
*/
client : '=',
/**
* @type ManagedFilesystem
*/
filesystem : '='
},
templateUrl: 'app/client/templates/guacFileBrowser.html',
controller: ['$scope', '$element', '$injector', function guacFileBrowserController($scope, $element, $injector) {
// Required types
var ManagedFilesystem = $injector.get('ManagedFilesystem');
// Required services
var $interpolate = $injector.get('$interpolate');
var $templateRequest = $injector.get('$templateRequest');
/**
* The jQuery-wrapped element representing the contents of the
* current directory within the file browser.
*
* @type Element[]
*/
var currentDirectoryContents = $element.find('.current-directory-contents');
/**
* Statically-cached template HTML used to render each file within
* a directory. Once available, this will be used through
* createFileElement() to generate the DOM elements which make up
* a directory listing.
*
* @type String
*/
var fileTemplate = null;
/**
* Returns whether the given file is a normal file.
*
* @param {ManagedFilesystem.File} file
* The file to test.
*
* @returns {Boolean}
* true if the given file is a normal file, false otherwise.
*/
$scope.isNormalFile = function isNormalFile(file) {
return file.type === ManagedFilesystem.File.Type.NORMAL;
};
/**
* Returns whether the given file is a directory.
*
* @param {ManagedFilesystem.File} file
* The file to test.
*
* @returns {Boolean}
* true if the given file is a directory, false otherwise.
*/
$scope.isDirectory = function isDirectory(file) {
return file.type === ManagedFilesystem.File.Type.DIRECTORY;
};
/**
* Changes the currently-displayed directory to the given
* directory.
*
* @param {ManagedFilesystem.File} file
* The directory to change to.
*/
$scope.changeDirectory = function changeDirectory(file) {
ManagedFilesystem.changeDirectory($scope.filesystem, file);
};
/**
* Initiates a download of the given file. The progress of the
* download can be observed through guacFileTransferManager.
*
* @param {ManagedFilesystem.File} file
* The file to download.
*/
$scope.downloadFile = function downloadFile(file) {
ManagedFilesystem.downloadFile($scope.client, $scope.filesystem, file.streamName);
};
/**
* Recursively interpolates all text nodes within the DOM tree of
* the given element. All other node types, attributes, etc. will
* be left uninterpolated.
*
* @param {Element} element
* The element at the root of the DOM tree to be interpolated.
*
* @param {Object} context
* The evaluation context to use when evaluating expressions
* embedded in text nodes within the provided element.
*/
var interpolateElement = function interpolateElement(element, context) {
// Interpolate the contents of text nodes directly
if (element.nodeType === Node.TEXT_NODE)
element.nodeValue = $interpolate(element.nodeValue)(context);
// Recursively interpolate the contents of all descendant text
// nodes
if (element.hasChildNodes()) {
var children = element.childNodes;
for (var i = 0; i < children.length; i++)
interpolateElement(children[i], context);
}
};
/**
* Creates a new element representing the given file and properly
* handling user events, bypassing the overhead incurred through
* use of ngRepeat and related techniques.
*
* Note that this function depends on the availability of the
* statically-cached fileTemplate.
*
* @param {ManagedFilesystem.File} file
* The file to generate an element for.
*
* @returns {Element[]}
* A jQuery-wrapped array containing a single DOM element
* representing the given file.
*/
var createFileElement = function createFileElement(file) {
// Create from internal template
var element = angular.element(fileTemplate);
interpolateElement(element[0], file);
// Double-clicking on unknown file types will do nothing
var fileAction = function doNothing() {};
// Change current directory when directories are clicked
if ($scope.isDirectory(file)) {
element.addClass('directory');
fileAction = function changeDirectory() {
$scope.changeDirectory(file);
};
}
// Initiate downloads when normal files are clicked
else if ($scope.isNormalFile(file)) {
element.addClass('normal-file');
fileAction = function downloadFile() {
$scope.downloadFile(file);
};
}
// Mark file as focused upon click
element.on('click', function handleFileClick() {
// Fire file-specific action if already focused
if (element.hasClass('focused')) {
fileAction();
element.removeClass('focused');
}
// Otherwise mark as focused
else {
element.parent().children().removeClass('focused');
element.addClass('focused');
}
});
// Prevent text selection during navigation
element.on('selectstart', function avoidSelect(e) {
e.preventDefault();
e.stopPropagation();
});
return element;
};
/**
* Sorts the given map of files, returning an array of those files
* grouped by file type (directories first, followed by non-
* directories) and sorted lexicographically.
*
* @param {Object.<String, ManagedFilesystem.File>} files
* The map of files to sort.
*
* @returns {ManagedFilesystem.File[]}
* An array of all files in the given map, sorted
* lexicographically with directories first, followed by non-
* directories.
*/
var sortFiles = function sortFiles(files) {
// Get all given files as an array
var unsortedFiles = [];
for (var name in files)
unsortedFiles.push(files[name]);
// Sort files - directories first, followed by all other files
// sorted by name
return unsortedFiles.sort(function fileComparator(a, b) {
// Directories come before non-directories
if ($scope.isDirectory(a) && !$scope.isDirectory(b))
return -1;
// Non-directories come after directories
if (!$scope.isDirectory(a) && $scope.isDirectory(b))
return 1;
// All other combinations are sorted by name
return a.name.localeCompare(b.name);
});
};
// Watch directory contents once file template is available
$templateRequest('app/client/templates/file.html').then(function fileTemplateRetrieved(html) {
// Store file template statically
fileTemplate = html;
// Update the contents of the file browser whenever the current directory (or its contents) changes
$scope.$watch('filesystem.currentDirectory.files', function currentDirectoryChanged(files) {
// Clear current content
currentDirectoryContents.html('');
// Display all files within current directory, sorted
angular.forEach(sortFiles(files), function displayFile(file) {
currentDirectoryContents.append(createFileElement(file));
});
});
}, angular.noop); // end retrieve file template
// Refresh file browser when any upload completes
$scope.$on('guacUploadComplete', function uploadComplete(event, filename) {
// Refresh filesystem, if it exists
if ($scope.filesystem)
ManagedFilesystem.refresh($scope.filesystem, $scope.filesystem.currentDirectory);
});
}]
};
}]);

View File

@@ -0,0 +1,234 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Directive which displays an active file transfer, providing links for
* downloads, if applicable.
*/
angular.module('client').directive('guacFileTransfer', [function guacFileTransfer() {
return {
restrict: 'E',
replace: true,
scope: {
/**
* The file transfer to display.
*
* @type ManagedFileUpload|ManagedFileDownload
*/
transfer : '='
},
templateUrl: 'app/client/templates/guacFileTransfer.html',
controller: ['$scope', '$injector', function guacFileTransferController($scope, $injector) {
// Required types
var ManagedFileTransferState = $injector.get('ManagedFileTransferState');
/**
* All upload error codes handled and passed off for translation.
* Any error code not present in this list will be represented by
* the "DEFAULT" translation.
*/
var UPLOAD_ERRORS = {
0x0100: true,
0x0201: true,
0x0202: true,
0x0203: true,
0x0204: true,
0x0205: true,
0x0301: true,
0x0303: true,
0x0308: true,
0x031D: true
};
/**
* Returns the unit string that is most appropriate for the
* number of bytes transferred thus far - either 'gb', 'mb', 'kb',
* or 'b'.
*
* @returns {String}
* The unit string that is most appropriate for the number of
* bytes transferred thus far.
*/
$scope.getProgressUnit = function getProgressUnit() {
var bytes = $scope.transfer.progress;
// Gigabytes
if (bytes > 1000000000)
return 'gb';
// Megabytes
if (bytes > 1000000)
return 'mb';
// Kilobytes
if (bytes > 1000)
return 'kb';
// Bytes
return 'b';
};
/**
* Returns the amount of data transferred thus far, in the units
* returned by getProgressUnit().
*
* @returns {Number}
* The amount of data transferred thus far, in the units
* returned by getProgressUnit().
*/
$scope.getProgressValue = function getProgressValue() {
var bytes = $scope.transfer.progress;
if (!bytes)
return bytes;
// Convert bytes to necessary units
switch ($scope.getProgressUnit()) {
// Gigabytes
case 'gb':
return (bytes / 1000000000).toFixed(1);
// Megabytes
case 'mb':
return (bytes / 1000000).toFixed(1);
// Kilobytes
case 'kb':
return (bytes / 1000).toFixed(1);
// Bytes
case 'b':
default:
return bytes;
}
};
/**
* Returns the percentage of bytes transferred thus far, if the
* overall length of the file is known.
*
* @returns {Number}
* The percentage of bytes transferred thus far, if the
* overall length of the file is known.
*/
$scope.getPercentDone = function getPercentDone() {
return $scope.transfer.progress / $scope.transfer.length * 100;
};
/**
* Determines whether the associated file transfer is in progress.
*
* @returns {Boolean}
* true if the file transfer is in progress, false othherwise.
*/
$scope.isInProgress = function isInProgress() {
// Not in progress if there is no transfer
if (!$scope.transfer)
return false;
// Determine in-progress status based on stream state
switch ($scope.transfer.transferState.streamState) {
// IDLE or OPEN file transfers are active
case ManagedFileTransferState.StreamState.IDLE:
case ManagedFileTransferState.StreamState.OPEN:
return true;
// All others are not active
default:
return false;
}
};
/**
* Returns whether the file associated with this file transfer can
* be saved locally via a call to save().
*
* @returns {Boolean}
* true if a call to save() will result in the file being
* saved, false otherwise.
*/
$scope.isSavable = function isSavable() {
return !!$scope.transfer.blob;
};
/**
* Saves the downloaded file, if any. If this transfer is an upload
* or the download is not yet complete, this function has no
* effect.
*/
$scope.save = function save() {
// Ignore if no blob exists
if (!$scope.transfer.blob)
return;
// Save file
saveAs($scope.transfer.blob, $scope.transfer.filename);
};
/**
* Returns whether an error has occurred. If an error has occurred,
* the transfer is no longer active, and the text of the error can
* be read from getErrorText().
*
* @returns {Boolean}
* true if an error has occurred during transfer, false
* otherwise.
*/
$scope.hasError = function hasError() {
return $scope.transfer.transferState.streamState === ManagedFileTransferState.StreamState.ERROR;
};
/**
* Returns the text of the current error as a translation string.
*
* @returns {String}
* The name of the translation string containing the text
* associated with the current error.
*/
$scope.getErrorText = function getErrorText() {
// Determine translation name of error
var status = $scope.transfer.transferState.statusCode;
var errorName = (status in UPLOAD_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT";
// Return translation string
return 'CLIENT.ERROR_UPLOAD_' + errorName;
};
}] // end file transfer controller
};
}]);

View File

@@ -0,0 +1,91 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Directive which displays all active file transfers.
*/
angular.module('client').directive('guacFileTransferManager', [function guacFileTransferManager() {
return {
restrict: 'E',
replace: true,
scope: {
/**
* The client whose file transfers should be managed by this
* directive.
*
* @type ManagerClient
*/
client : '='
},
templateUrl: 'app/client/templates/guacFileTransferManager.html',
controller: ['$scope', '$injector', function guacFileTransferManagerController($scope, $injector) {
// Required types
var ManagedFileTransferState = $injector.get('ManagedFileTransferState');
/**
* Determines whether the given file transfer state indicates an
* in-progress transfer.
*
* @param {ManagedFileTransferState} transferState
* The file transfer state to check.
*
* @returns {Boolean}
* true if the given file transfer state indicates an in-
* progress transfer, false otherwise.
*/
var isInProgress = function isInProgress(transferState) {
switch (transferState.streamState) {
// IDLE or OPEN file transfers are active
case ManagedFileTransferState.StreamState.IDLE:
case ManagedFileTransferState.StreamState.OPEN:
return true;
// All others are not active
default:
return false;
}
};
/**
* Removes all file transfers which are not currently in-progress.
*/
$scope.clearCompletedTransfers = function clearCompletedTransfers() {
// Nothing to clear if no client attached
if (!$scope.client)
return;
// Remove completed uploads
$scope.client.uploads = $scope.client.uploads.filter(function isUploadInProgress(upload) {
return isInProgress(upload.transferState);
});
};
}]
};
}]);

View File

@@ -0,0 +1,159 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive for displaying a Guacamole client as a non-interactive
* thumbnail.
*/
angular.module('client').directive('guacThumbnail', [function guacThumbnail() {
return {
// Element only
restrict: 'E',
replace: true,
scope: {
/**
* The client to display within this guacThumbnail directive.
*
* @type ManagedClient
*/
client : '='
},
templateUrl: 'app/client/templates/guacThumbnail.html',
controller: ['$scope', '$injector', '$element', function guacThumbnailController($scope, $injector, $element) {
// Required services
var $window = $injector.get('$window');
/**
* The optimal thumbnail width, in pixels.
*
* @type Number
*/
var THUMBNAIL_WIDTH = 320;
/**
* The optimal thumbnail height, in pixels.
*
* @type Number
*/
var THUMBNAIL_HEIGHT = 240;
/**
* The display of the current Guacamole client instance.
*
* @type Guacamole.Display
*/
var display = null;
/**
* The element associated with the display of the current
* Guacamole client instance.
*
* @type Element
*/
var displayElement = null;
/**
* The element which must contain the Guacamole display element.
*
* @type Element
*/
var displayContainer = $element.find('.display')[0];
/**
* The main containing element for the entire directive.
*
* @type Element
*/
var main = $element[0];
/**
* Updates the scale of the attached Guacamole.Client based on current window
* size and "auto-fit" setting.
*/
$scope.updateDisplayScale = function updateDisplayScale() {
if (!display) return;
// Fit within available area
display.scale(Math.min(
main.offsetWidth / Math.max(display.getWidth(), 1),
main.offsetHeight / Math.max(display.getHeight(), 1)
));
};
// Attach any given managed client
$scope.$watch('client', function attachManagedClient(managedClient) {
// Remove any existing display
displayContainer.innerHTML = "";
// Only proceed if a client is given
if (!managedClient)
return;
// Get Guacamole client instance
var client = managedClient.client;
// Attach possibly new display
display = client.getDisplay();
// Add display element
displayElement = display.getElement();
displayContainer.appendChild(displayElement);
});
// Update scale when display is resized
$scope.$watch('client.managedDisplay.size', function setDisplaySize(size) {
var width;
var height;
// If no display size yet, assume optimal thumbnail size
if (!size || size.width === 0 || size.height === 0) {
width = THUMBNAIL_WIDTH;
height = THUMBNAIL_HEIGHT;
}
// Otherwise, generate size that fits within thumbnail bounds
else {
var scale = Math.min(THUMBNAIL_WIDTH / size.width, THUMBNAIL_HEIGHT / size.height, 1);
width = size.width * scale;
height = size.height * scale;
}
// Generate dummy background image
var thumbnail = document.createElement("canvas");
thumbnail.width = width;
thumbnail.height = height;
$scope.thumbnail = thumbnail.toDataURL("image/png");
// Init display scale
$scope.$evalAsync($scope.updateDisplayScale);
});
}]
};
}]);

View File

@@ -0,0 +1,112 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive which provides a fullscreen environment for its content.
*/
angular.module('client').directive('guacViewport', [function guacViewport() {
return {
// Element only
restrict: 'E',
scope: {},
transclude: true,
templateUrl: 'app/client/templates/guacViewport.html',
controller: ['$scope', '$injector', '$element',
function guacViewportController($scope, $injector, $element) {
// Required services
var $window = $injector.get('$window');
/**
* The fullscreen container element.
*
* @type Element
*/
var element = $element.find('.viewport')[0];
/**
* The width of the browser viewport when fitVisibleArea() was last
* invoked, in pixels, or null if fitVisibleArea() has not yet been
* called.
*
* @type Number
*/
var lastViewportWidth = null;
/**
* The height of the browser viewport when fitVisibleArea() was
* last invoked, in pixels, or null if fitVisibleArea() has not yet
* been called.
*
* @type Number
*/
var lastViewportHeight = null;
/**
* Resizes the container element inside the guacViewport such that
* it exactly fits within the visible area, even if the browser has
* been scrolled.
*/
var fitVisibleArea = function fitVisibleArea() {
// Calculate viewport dimensions (this is NOT necessarily the
// same as 100vw and 100vh, 100%, etc., particularly when the
// on-screen keyboard of a mobile device pops open)
var viewportWidth = $window.innerWidth;
var viewportHeight = $window.innerHeight;
// Adjust element width to fit exactly within visible area
if (viewportWidth !== lastViewportWidth) {
element.style.width = viewportWidth + 'px';
lastViewportWidth = viewportWidth;
}
// Adjust element height to fit exactly within visible area
if (viewportHeight !== lastViewportHeight) {
element.style.height = viewportHeight + 'px';
lastViewportHeight = viewportHeight;
}
// Scroll element such that its upper-left corner is exactly
// within the viewport upper-left corner, if not already there
if (element.scrollLeft || element.scrollTop) {
$window.scrollTo(
$window.pageXOffset + element.scrollLeft,
$window.pageYOffset + element.scrollTop
);
}
};
// Fit container within visible region when window scrolls
$window.addEventListener('scroll', fitVisibleArea);
// Poll every 10ms, in case scroll event does not fire
var pollArea = $window.setInterval(fitVisibleArea, 10);
// Clean up on destruction
$scope.$on('$destroy', function destroyViewport() {
$window.removeEventListener('scroll', fitVisibleArea);
$window.clearInterval(pollArea);
});
}]
};
}]);

View File

@@ -0,0 +1,48 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive which converts between human-readable zoom
* percentage and display scale.
*/
angular.module('client').directive('guacZoomCtrl', function guacZoomCtrl() {
return {
restrict: 'A',
require: 'ngModel',
priority: 101,
link: function(scope, element, attrs, ngModel) {
// Evaluate the ngChange attribute when the model
// changes.
ngModel.$viewChangeListeners.push(function() {
scope.$eval(attrs.ngChange);
});
// When pushing to the menu, mutiply by 100.
ngModel.$formatters.push(function(value) {
return Math.round(value * 100);
});
// When parsing value from menu, divide by 100.
ngModel.$parsers.push(function(value) {
return Math.round(value) / 100;
});
}
}
});

View File

@@ -0,0 +1,39 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A service for checking browser audio support.
*/
angular.module('client').factory('guacAudio', [function guacAudio() {
/**
* Object describing the UI's level of audio support.
*/
return new (function() {
/**
* Array of all supported audio mimetypes.
*
* @type String[]
*/
this.supported = Guacamole.AudioPlayer.getSupportedTypes();
})();
}]);

View File

@@ -0,0 +1,169 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A service for managing several active Guacamole clients.
*/
angular.module('client').factory('guacClientManager', ['$injector',
function guacClientManager($injector) {
// Required types
var ManagedClient = $injector.get('ManagedClient');
// Required services
var $window = $injector.get('$window');
var sessionStorageFactory = $injector.get('sessionStorageFactory');
var service = {};
/**
* Getter/setter which retrieves or sets the map of all active managed
* clients. Each key is the ID of the connection used by that client.
*
* @type Function
*/
var storedManagedClients = sessionStorageFactory.create({}, function destroyClientStorage() {
// Disconnect all clients when storage is destroyed
service.clear();
});
/**
* Returns a map of all active managed clients. Each key is the ID of the
* connection used by that client.
*
* @returns {Object.<String, ManagedClient>}
* A map of all active managed clients.
*/
service.getManagedClients = function getManagedClients() {
return storedManagedClients();
};
/**
* Removes the existing ManagedClient associated with the connection having
* the given ID, if any. If no such a ManagedClient already exists, this
* function has no effect.
*
* @param {String} id
* The ID of the connection whose ManagedClient should be removed.
*
* @returns {Boolean}
* true if an existing client was removed, false otherwise.
*/
service.removeManagedClient = function replaceManagedClient(id) {
var managedClients = storedManagedClients();
// Remove client if it exists
if (id in managedClients) {
// Disconnect and remove
managedClients[id].client.disconnect();
delete managedClients[id];
// A client was removed
return true;
}
// No client was removed
return false;
};
/**
* Creates a new ManagedClient associated with the connection having the
* given ID. If such a ManagedClient already exists, it is disconnected and
* replaced.
*
* @param {String} id
* The ID of the connection whose ManagedClient should be retrieved.
*
* @param {String} [connectionParameters]
* Any additional HTTP parameters to pass while connecting. This
* parameter only has an effect if a new connection is established as
* a result of this function call.
*
* @returns {ManagedClient}
* The ManagedClient associated with the connection having the given
* ID.
*/
service.replaceManagedClient = function replaceManagedClient(id, connectionParameters) {
// Disconnect any existing client
service.removeManagedClient(id);
// Set new client
return storedManagedClients()[id] = ManagedClient.getInstance(id, connectionParameters);
};
/**
* Returns the ManagedClient associated with the connection having the
* given ID. If no such ManagedClient exists, a new ManagedClient is
* created.
*
* @param {String} id
* The ID of the connection whose ManagedClient should be retrieved.
*
* @param {String} [connectionParameters]
* Any additional HTTP parameters to pass while connecting. This
* parameter only has an effect if a new connection is established as
* a result of this function call.
*
* @returns {ManagedClient}
* The ManagedClient associated with the connection having the given
* ID.
*/
service.getManagedClient = function getManagedClient(id, connectionParameters) {
var managedClients = storedManagedClients();
// Create new managed client if it doesn't already exist
if (!(id in managedClients))
managedClients[id] = ManagedClient.getInstance(id, connectionParameters);
// Return existing client
return managedClients[id];
};
/**
* Disconnects and removes all currently-connected clients.
*/
service.clear = function clear() {
var managedClients = storedManagedClients();
// Disconnect each managed client
for (var id in managedClients)
managedClients[id].client.disconnect();
// Clear managed clients
storedManagedClients({});
};
// Disconnect all clients when window is unloaded
$window.addEventListener('unload', service.clear);
return service;
}]);

View File

@@ -0,0 +1,135 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A service for checking browser image support.
*/
angular.module('client').factory('guacImage', ['$injector', function guacImage($injector) {
// Required services
var $q = $injector.get('$q');
var service = {};
/**
* Map of possibly-supported image mimetypes to corresponding test images
* encoded with base64. If the image is correctly decoded, it will be a
* single pixel (1x1) image.
*
* @type Object.<String, String>
*/
var testImages = {
/**
* Test JPEG image, encoded as base64.
*/
'image/jpeg' :
'/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoH'
+ 'BwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQME'
+ 'BAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU'
+ 'FBQUFBQUFBQUFBQUFBT/wAARCAABAAEDAREAAhEBAxEB/8QAFAABAAAAAAAAAAA'
+ 'AAAAAAAAACf/EABQQAQAAAAAAAAAAAAAAAAAAAAD/xAAUAQEAAAAAAAAAAAAAAA'
+ 'AAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AVMH/2Q==',
/**
* Test PNG image, encoded as base64.
*/
'image/png' :
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX///+nxBvI'
+ 'AAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==',
/**
* Test WebP image, encoded as base64.
*/
'image/webp' : 'UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA=='
};
/**
* Deferred which tracks the progress and ultimate result of all pending
* image format tests.
*
* @type Deferred
*/
var deferredSupportedMimetypes = $q.defer();
/**
* Array of all promises associated with pending image tests. Each image
* test promise MUST be guaranteed to resolve and MUST NOT be rejected.
*
* @type Promise[]
*/
var pendingTests = [];
/**
* The array of supported image formats. This will be gradually populated
* by the various image tests that occur in the background, and will not be
* fully populated until all promises within pendingTests are resolved.
*
* @type String[]
*/
var supported = [];
/**
* Return a promise which resolves with to an array of image mimetypes
* supported by the browser, once those mimetypes are known. The returned
* promise is guaranteed to resolve successfully.
*
* @returns {Promise.<String[]>}
* A promise which resolves with an array of image mimetypes supported
* by the browser.
*/
service.getSupportedMimetypes = function getSupportedMimetypes() {
return deferredSupportedMimetypes.promise;
};
// Test each possibly-supported image
angular.forEach(testImages, function testImageSupport(data, mimetype) {
// Add promise for current image test
var imageTest = $q.defer();
pendingTests.push(imageTest.promise);
// Attempt to load image
var image = new Image();
image.src = 'data:' + mimetype + ';base64,' + data;
// Store as supported depending on whether load was successful
image.onload = image.onerror = function imageTestComplete() {
// Image format is supported if successfully decoded
if (image.width === 1 && image.height === 1)
supported.push(mimetype);
// Test is complete
imageTest.resolve();
};
});
// When all image tests are complete, resolve promise with list of
// supported formats
$q.all(pendingTests).then(function imageTestsCompleted() {
deferredSupportedMimetypes.resolve(supported);
});
return service;
}]);

View File

@@ -0,0 +1,37 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A service for checking browser video support.
*/
angular.module('client').factory('guacVideo', [function guacVideo() {
/**
* Object describing the UI's level of video support.
*/
return new (function() {
/**
* Array of all supported video mimetypes.
*/
this.supported = Guacamole.VideoPlayer.getSupportedTypes();
})();
}]);

View File

@@ -0,0 +1,129 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
body.client {
background: black;
padding: 0;
margin: 0;
overflow: hidden;
}
#preload {
visibility: hidden;
position: absolute;
left: 0;
right: 0;
width: 0;
height: 0;
overflow: hidden;
}
.client-view {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
font-size: 0px;
}
.client-view-content {
/* IE10 */
display: -ms-flexbox;
-ms-flex-align: stretch;
-ms-flex-direction: column;
-ms-flex-pack: end;
/* Ancient Mozilla */
display: -moz-box;
-moz-box-align: stretch;
-moz-box-orient: vertical;
-moz-box-pack: end;
/* Ancient WebKit */
display: -webkit-box;
-webkit-box-align: stretch;
-webkit-box-orient: vertical;
-webkit-box-pack: end;
/* Old WebKit */
display: -webkit-flex;
-webkit-align-items: stretch;
-webkit-flex-direction: column;
-webkit-flex-pack: end;
/* W3C */
display: flex;
align-items: stretch;
flex-direction: column;
flex-pack: end;
width: 100%;
height: 100%;
font-size: 12pt;
}
.client-view .client-body {
-ms-flex: 1 1 auto;
-moz-box-flex: 1;
-webkit-box-flex: 1;
-webkit-flex: 1 1 auto;
flex: 1 1 auto;
position: relative;
}
.client-view .client-bottom {
-ms-flex: 0 0 auto;
-moz-box-flex: 0;
-webkit-box-flex: 0;
-webkit-flex: 0 0 auto;
flex: 0 0 auto;
}
.client-view .client-body .main {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
width: auto;
height: auto;
}
.client .menu .header h2 {
text-transform: none;
}
.client .user-menu .menu-contents li a.disconnect {
background-repeat: no-repeat;
background-size: 1em;
background-position: 0.75em center;
padding-left: 2.5em;
background-image: url('images/x.png');
}

View File

@@ -0,0 +1,57 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#guac-menu .header h2.connection-select-menu {
overflow: visible;
}
.connection-select-menu {
padding: 0;
min-width: 0;
}
.connection-select-menu .menu-dropdown {
border: none;
}
.connection-select-menu .menu-dropdown .menu-contents {
font-weight: normal;
font-size: 0.8em;
right: auto;
left: 0;
max-width: 100vw;
width: 400px;
}
.connection-select-menu .menu-dropdown .menu-contents .filter input {
border-bottom: 1px solid rgba(0,0,0,0.125);
border-left: none;
}
.connection-select-menu .menu-dropdown .menu-contents .filter {
margin-bottom: 0.5em;
padding: 0;
}
.connection-select-menu .menu-dropdown .menu-contents .group-list .caption {
display: inline-block;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}

View File

@@ -0,0 +1,56 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#connection-warning {
position: absolute;
right: 0.25em;
bottom: 0.25em;
z-index: 20;
width: 3in;
max-width: 100%;
min-height: 1em;
border-left: 2em solid #FA0;
box-shadow: 1px 1px 2px rgba(0,0,0,0.25);
background: #FFE;
padding: 0.5em 0.75em;
font-size: .8em;
}
#connection-warning::before {
content: ' ';
display: block;
position: absolute;
left: -2em;
top: 0;
width: 1.25em;
height: 100%;
margin: 0 0.375em;
background: url('images/warning.png');
background-size: contain;
background-position: center;
background-repeat: no-repeat;
}

View File

@@ -0,0 +1,66 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.software-cursor {
cursor: url('images/mouse/blank.gif'),url('images/mouse/blank.cur'),default;
overflow: hidden;
cursor: none;
}
.guac-error .software-cursor {
cursor: default;
}
div.main {
overflow: auto;
width: 100%;
height: 100%;
position: relative;
font-size: 0px;
}
div.displayOuter {
height: 100%;
width: 100%;
position: absolute;
left: 0;
top: 0;
display: table;
}
div.displayMiddle {
width: 100%;
height: 100%;
display: table-cell;
vertical-align: middle;
text-align: center;
}
div.display {
display: inline-block;
}
div.display * {
position: relative;
}
div.display > * {
margin-left: auto;
margin-right: auto;
}

View File

@@ -0,0 +1,49 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/* Hide directory contents by default */
.file-browser .directory > .children {
padding-left: 1em;
display: none;
}
.file-browser .list-item .caption {
white-space: nowrap;
border: 1px solid transparent;
}
.file-browser .list-item.focused .caption {
border: 1px dotted rgba(0, 0, 0, 0.5);
background: rgba(204, 221, 170, 0.5);
}
/* Directory / file icons */
.file-browser .normal-file > .caption .icon {
background-image: url('images/file.png');
}
.file-browser .directory > .caption .icon {
background-image: url('images/folder-closed.png');
}
.file-browser .directory.previous > .caption .icon {
background-image: url('images/folder-up.png');
}

View File

@@ -0,0 +1,118 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#file-transfer-dialog {
position: absolute;
right: 0;
bottom: 0;
z-index: 20;
font-size: 0.8em;
width: 4in;
max-width: 100%;
max-height: 3in;
}
#file-transfer-dialog .transfer-manager {
/* IE10 */
display: -ms-flexbox;
-ms-flex-align: stretch;
-ms-flex-direction: column;
/* Ancient Mozilla */
display: -moz-box;
-moz-box-align: stretch;
-moz-box-orient: vertical;
/* Ancient WebKit */
display: -webkit-box;
-webkit-box-align: stretch;
-webkit-box-orient: vertical;
/* Old WebKit */
display: -webkit-flex;
-webkit-align-items: stretch;
-webkit-flex-direction: column;
/* W3C */
display: flex;
align-items: stretch;
flex-direction: column;
max-width: inherit;
max-height: inherit;
border: 1px solid rgba(0, 0, 0, 0.5);
box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.25);
}
#file-transfer-dialog .transfer-manager .header {
-ms-flex: 0 0 auto;
-moz-box-flex: 0;
-webkit-box-flex: 0;
-webkit-flex: 0 0 auto;
flex: 0 0 auto;
}
#file-transfer-dialog .transfer-manager .transfer-manager-body {
-ms-flex: 1 1 auto;
-moz-box-flex: 1;
-webkit-box-flex: 1;
-webkit-flex: 1 1 auto;
flex: 1 1 auto;
overflow: auto;
}
/*
* Shrink maximum height if viewport is too small for default 3in dialog.
*/
@media all and (max-height: 3in) {
#file-transfer-dialog {
max-height: 1.5in;
}
}
/*
* If viewport is too small for even the 1.5in dialog, fit all available space.
*/
@media all and (max-height: 1.5in) {
#file-transfer-dialog {
height: 100%;
}
#file-transfer-dialog .transfer-manager {
position: absolute;
left: 0.5em;
top: 0.5em;
right: 0.5em;
bottom: 0.5em;
}
}

View File

@@ -0,0 +1,72 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#filesystem-menu .header h2 {
font-size: 1em;
font-weight: normal;
padding-top: 0;
padding-bottom: 0;
}
#filesystem-menu .header {
-ms-flex-align: center;
-moz-box-align: center;
-webkit-box-align: center;
-webkit-align-items: center;
align-items: center;
}
#filesystem-menu .menu-body {
padding: 0.25em;
}
#filesystem-menu .header.breadcrumbs {
display: block;
background: rgba(0,0,0,0.0125);
border-bottom: 1px solid rgba(0,0,0,0.05);
box-shadow: none;
margin-top: 0;
border-top: none;
}
#filesystem-menu .header.breadcrumbs .breadcrumb {
display: inline-block;
padding: 0.5em;
font-size: 0.8em;
font-weight: bold;
}
#filesystem-menu .header.breadcrumbs .breadcrumb:hover {
background-color: #CDA;
cursor: pointer;
}
#filesystem-menu .header.breadcrumbs .breadcrumb.root {
background-size: 1.5em 1.5em;
-moz-background-size: 1.5em 1.5em;
-webkit-background-size: 1.5em 1.5em;
-khtml-background-size: 1.5em 1.5em;
background-repeat: no-repeat;
background-position: center center;
background-image: url('images/drive.png');
width: 2em;
height: 2em;
padding: 0;
vertical-align: middle;
}

View File

@@ -0,0 +1,208 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#guac-menu .content {
padding: 0;
margin: 0;
/* IE10 */
display: -ms-flexbox;
-ms-flex-align: stretch;
-ms-flex-direction: column;
/* Ancient Mozilla */
display: -moz-box;
-moz-box-align: stretch;
-moz-box-orient: vertical;
/* Ancient WebKit */
display: -webkit-box;
-webkit-box-align: stretch;
-webkit-box-orient: vertical;
/* Old WebKit */
display: -webkit-flex;
-webkit-align-items: stretch;
-webkit-flex-direction: column;
/* W3C */
display: flex;
align-items: stretch;
flex-direction: column;
}
#guac-menu .content > * {
margin: 0;
-ms-flex: 0 0 auto;
-moz-box-flex: 0;
-webkit-box-flex: 0;
-webkit-flex: 0 0 auto;
flex: 0 0 auto;
}
#guac-menu .content > * + * {
margin-top: 1em;
}
#guac-menu .header h2 {
white-space: nowrap;
overflow: hidden;
width: 100%;
text-overflow: ellipsis;
}
#guac-menu #mouse-settings .choice {
text-align: center;
}
#guac-menu #mouse-settings .choice .figure {
display: inline-block;
vertical-align: middle;
width: 75%;
max-width: 320px;
}
#guac-menu #keyboard-settings .caption {
font-size: 0.9em;
margin-left: 2em;
margin-right: 2em;
}
#guac-menu #mouse-settings .figure .caption {
text-align: center;
font-size: 0.9em;
}
#guac-menu #mouse-settings .figure img {
display: block;
width: 100%;
max-width: 320px;
margin: 1em auto;
}
#guac-menu #keyboard-settings .figure {
float: right;
max-width: 30%;
margin: 1em;
}
#guac-menu #keyboard-settings .figure img {
max-width: 100%;
}
#guac-menu #zoom-settings {
text-align: center;
}
#guac-menu #zoom-out,
#guac-menu #zoom-in,
#guac-menu #zoom-state {
display: inline-block;
vertical-align: middle;
}
#guac-menu #zoom-out,
#guac-menu #zoom-in {
max-width: 3em;
border: 1px solid rgba(0, 0, 0, 0.5);
background: rgba(0, 0, 0, 0.1);
border-radius: 2em;
margin: 0.5em;
cursor: pointer;
}
#guac-menu #zoom-out img,
#guac-menu #zoom-in img {
max-width: 100%;
opacity: 0.5;
}
#guac-menu #zoom-out:hover,
#guac-menu #zoom-in:hover {
border: 1px solid rgba(0, 0, 0, 1);
background: #CDA;
}
#guac-menu #zoom-out:hover img,
#guac-menu #zoom-in:hover img {
opacity: 1;
}
#guac-menu #zoom-state {
font-size: 2em;
}
#guac-menu #devices .device {
padding: 1em;
border: 1px solid rgba(0, 0, 0, 0.125);
background: rgba(0, 0, 0, 0.04);
padding-left: 3.5em;
background-size: 1.5em 1.5em;
-moz-background-size: 1.5em 1.5em;
-webkit-background-size: 1.5em 1.5em;
-khtml-background-size: 1.5em 1.5em;
background-repeat: no-repeat;
background-position: 1em center;
}
#guac-menu #devices .device:hover {
cursor: pointer;
border-color: black;
}
#guac-menu #devices .device.filesystem {
background-image: url('images/drive.png');
}
#guac-menu #share-links {
padding: 1em;
border: 1px solid rgba(0, 0, 0, 0.125);
background: rgba(0, 0, 0, 0.04);
font-size: 0.8em;
}
#guac-menu #share-links h3 {
padding-bottom: 0;
}
#guac-menu #share-links th {
white-space: nowrap;
}
#guac-menu #share-links a[href] {
display: block;
padding: 0 1em;
font-family: monospace;
font-weight: bold;
}

View File

@@ -0,0 +1,34 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.keyboard-container {
text-align: center;
width: 100%;
margin: 0;
padding: 0;
border-top: 1px solid black;
background: #222;
opacity: 0.85;
z-index: 1;
}

View File

@@ -0,0 +1,167 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.menu {
overflow: hidden;
position: absolute;
top: 0;
height: 100%;
max-width: 100%;
width: 480px;
background: #EEE;
box-shadow: inset -1px 0 2px white, 1px 0 2px black;
z-index: 10;
-webkit-transition: left 0.125s, opacity 0.125s;
-moz-transition: left 0.125s, opacity 0.125s;
-ms-transition: left 0.125s, opacity 0.125s;
-o-transition: left 0.125s, opacity 0.125s;
transition: left 0.125s, opacity 0.125s;
}
.menu-content {
/* IE10 */
display: -ms-flexbox;
-ms-flex-align: stretch;
-ms-flex-direction: column;
/* Ancient Mozilla */
display: -moz-box;
-moz-box-align: stretch;
-moz-box-orient: vertical;
/* Ancient WebKit */
display: -webkit-box;
-webkit-box-align: stretch;
-webkit-box-orient: vertical;
/* Old WebKit */
display: -webkit-flex;
-webkit-align-items: stretch;
-webkit-flex-direction: column;
/* W3C */
display: flex;
align-items: stretch;
flex-direction: column;
width: 100%;
height: 100%;
}
.menu-content .header {
-ms-flex: 0 0 auto;
-moz-box-flex: 0;
-webkit-box-flex: 0;
-webkit-flex: 0 0 auto;
flex: 0 0 auto;
margin-bottom: 0;
}
.menu-body {
-ms-flex: 1 1 auto;
-moz-box-flex: 1;
-webkit-box-flex: 1;
-webkit-flex: 1 1 auto;
flex: 1 1 auto;
padding: 1em;
overflow: auto;
/* IE10 */
display: -ms-flexbox;
-ms-flex-align: stretch;
-ms-flex-direction: column;
/* Ancient Mozilla */
display: -moz-box;
-moz-box-align: stretch;
-moz-box-orient: vertical;
/* Ancient WebKit */
display: -webkit-box;
-webkit-box-align: stretch;
-webkit-box-orient: vertical;
/* Old WebKit */
display: -webkit-flex;
-webkit-align-items: stretch;
-webkit-flex-direction: column;
/* W3C */
display: flex;
align-items: stretch;
flex-direction: column;
}
.menu-body > * {
-ms-flex: 0 0 auto;
-moz-box-flex: 0;
-webkit-box-flex: 0;
-webkit-flex: 0 0 auto;
flex: 0 0 auto;
}
.menu-section h3 {
margin: 0;
padding: 0;
padding-bottom: 1em;
}
.menu-section ~ .menu-section h3 {
padding-top: 1em;
}
.menu-section input.zoom-ctrl {
width: 2em;
font-size: 1em;
padding: 0;
background: transparent;
border-color: rgba(0, 0, 0, 0.125);
}
.menu-section div.zoom-ctrl {
font-size: 1.5em;
display: inline;
align-content: center;
vertical-align: middle;
}
.menu-section .zoom-ctrl::-webkit-inner-spin-button,
.menu-section .zoom-ctrl::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
.menu,
.menu.closed {
left: -480px;
opacity: 0;
}
.menu.open {
left: 0px;
opacity: 1;
}

View File

@@ -0,0 +1,23 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.client .notification .parameters h3,
.client .notification .parameters .password-field .toggle-password {
display: none;
}

View File

@@ -0,0 +1,206 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#other-connections .client-panel {
display: none;
position: absolute;
right: 0;
bottom: 0;
border: 1px solid rgba(255, 255, 255, 0.25);
background: rgba(0, 0, 0, 0.25);
max-width: 100%;
white-space: nowrap;
transition: max-width 0.125s, width 0.125s;
/* Render above modal status */
z-index: 20;
}
#other-connections .client-panel.has-clients {
display: block;
}
#other-connections .client-panel.hidden {
max-width: 16px;
}
#other-connections .client-panel-handle {
position: absolute;
left: 0;
bottom: 0;
height: 100%;
width: 16px;
z-index: 1;
background-color: white;
background-repeat: no-repeat;
background-size: contain;
background-position: center center;
background-image: url(images/arrows/right.png);
opacity: 0.5;
}
#other-connections .client-panel-handle:hover {
opacity: 0.75;
}
#other-connections .client-panel.hidden .client-panel-handle {
background-image: url(images/arrows/left.png);
}
#other-connections .client-panel-connection-list {
text-align: right;
margin: 0;
padding: 0;
padding-left: 16px;
overflow-x: auto;
overflow-y: hidden;
}
#other-connections .client-panel-connection {
display: inline-block;
position: relative;
margin: 0.5em;
border: 1px solid white;
background: black;
box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.5);
opacity: 0.5;
transition: opacity 0.25s;
max-height: 128px;
overflow: hidden;
vertical-align: middle;
}
#other-connections .client-panel-connection .thumbnail-main img {
max-width: none;
max-height: 128px;
}
#other-connections .client-panel-connection a[href]::before {
display: block;
content: ' ';
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
z-index: 1;
background: url('images/warning-white.png');
background-size: 48px;
background-position: center;
background-repeat: no-repeat;
background-color: black;
opacity: 0;
transition: opacity 0.25s;
}
#other-connections .client-panel-connection.needs-attention a[href]::before {
opacity: 0.75;
}
#other-connections button.close-other-connection {
position: absolute;
top: 0;
right: 0;
z-index: 2;
margin: 0;
padding: 4px;
min-width: 0;
border: none;
background: transparent;
box-shadow: none;
text-shadow: none;
opacity: 0.5;
line-height: 1;
}
#other-connections button.close-other-connection:hover {
opacity: 1;
}
#other-connections button.close-other-connection img {
background: #A43;
border-radius: 18px;
max-width: 18px;
padding: 3px;
}
#other-connections button.close-other-connection:hover img {
background: #C54;
}
#other-connections .client-panel.hidden .client-panel-connection-list {
/* Hide scrollbar when panel is hidden (will be visible through panel
* show/hide button otherwise) */
overflow-x: hidden;
}
#other-connections .client-panel.hidden .client-panel-connection {
/* Hide thumbnails when panel is hidden (will be visible through panel
* show/hide button otherwise) */
visibility: hidden;
}
#other-connections .client-panel-connection .name {
position: absolute;
padding: 0.25em 0.5em;
left: 0;
right: 0;
bottom: 0;
z-index: 2;
text-align: left;
color: white;
background: rgba(0, 0, 0, 0.5);
font-size: 0.75em;
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#other-connections .client-panel-connection:hover {
opacity: 1;
}

View File

@@ -0,0 +1,58 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.share-menu {
/* IE10 */
display: -ms-flexbox;
-ms-flex-align: stretch;
-ms-flex-direction: row;
/* Ancient Mozilla */
display: -moz-box;
-moz-box-align: stretch;
-moz-box-orient: horizontal;
/* Ancient WebKit */
display: -webkit-box;
-webkit-box-align: stretch;
-webkit-box-orient: horizontal;
/* Old WebKit */
display: -webkit-flex;
-webkit-align-items: stretch;
-webkit-flex-direction: row;
/* W3C */
display: flex;
align-items: stretch;
flex-direction: row;
}
.share-menu .menu-dropdown .menu-title {
padding-left: 2em;
background-repeat: no-repeat;
background-size: 1em;
background-position: 0.5em center;
background-image: url('images/share.png');
}

View File

@@ -0,0 +1,35 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
div.thumbnail-main {
overflow: hidden;
width: 100%;
height: 100%;
position: relative;
font-size: 0px;
}
.thumbnail-main img {
max-width: 100%;
}
.thumbnail-main .display {
position: absolute;
pointer-events: none;
}

View File

@@ -0,0 +1,43 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.transfer-manager {
background: white;
}
.transfer-manager .header h2 {
font-size: 1em;
padding-top: 0;
padding-bottom: 0;
}
.transfer-manager .header {
margin: 0;
-ms-flex-align: center;
-moz-box-align: center;
-webkit-box-align: center;
-webkit-align-items: center;
align-items: center;
}
.transfer-manager .transfers {
display: table;
padding: 0.25em;
width: 100%;
}

View File

@@ -0,0 +1,132 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.transfer {
display: table-row;
}
.transfer .transfer-status {
display: table-cell;
padding: 0.25em;
position: relative;
}
.transfer .text {
display: table-cell;
text-align: right;
padding: 0.25em
}
.transfer .filename {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
position: relative;
font-family: monospace;
font-weight: bold;
padding: 0.125em;
}
@keyframes transfer-progress {
from {background-position: 0px 0px;}
to {background-position: 64px 0px;}
}
@-webkit-keyframes transfer-progress {
from {background-position: 0px 0px;}
to {background-position: 64px 0px;}
}
.transfer .progress {
width: 100%;
padding: 0.25em;
position: absolute;
top: 0;
left: 0;
bottom: 0;
opacity: 0.25;
}
.transfer.in-progress .progress {
background-color: #EEE;
background-image: url('images/progress.png');
background-size: 16px 16px;
-moz-background-size: 16px 16px;
-webkit-background-size: 16px 16px;
-khtml-background-size: 16px 16px;
animation-name: transfer-progress;
animation-duration: 2s;
animation-timing-function: linear;
animation-iteration-count: infinite;
-webkit-animation-name: transfer-progress;
-webkit-animation-duration: 2s;
-webkit-animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
}
.transfer .progress .bar {
display: none;
background: #A3D655;
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 0;
}
.transfer.in-progress .progress .bar {
display: initial;
}
.transfer.savable {
cursor: pointer;
}
.transfer.savable .filename {
color: blue;
text-decoration: underline;
}
.transfer.error {
background: #FDD;
}
.transfer.error .text,
.transfer.error .progress .bar {
display: none;
}
.transfer .error-text {
display: none;
}
.transfer.error .error-text {
display: block;
margin: 0;
margin-top: 0.5em;
width: 100%;
}

View File

@@ -0,0 +1,27 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.viewport {
position: absolute;
bottom: 0;
right: 0;
width: 100%;
height: 100%;
overflow: hidden;
}

View File

@@ -0,0 +1,235 @@
<guac-viewport>
<!-- Client view -->
<div class="client-view">
<div class="client-view-content">
<!-- Central portion of view -->
<div class="client-body" guac-touch-drag="clientDrag" guac-touch-pinch="clientPinch">
<!-- Client for current connection -->
<guac-client client="client"></guac-client>
<!-- All other active connections -->
<div id="other-connections">
<guac-client-panel clients="otherClients"></guac-client-panel>
</div>
</div>
<!-- Bottom portion of view -->
<div class="client-bottom">
<!-- Text input -->
<div class="text-input-container" ng-if="showTextInput">
<guac-text-input></guac-text-input>
</div>
<!-- On-screen keyboard -->
<div class="keyboard-container" ng-if="showOSK">
<guac-osk layout="'CLIENT.URL_OSK_LAYOUT' | translate"></guac-osk>
</div>
</div>
</div>
</div>
<!-- File transfers -->
<div id="file-transfer-dialog" ng-show="hasTransfers()">
<guac-file-transfer-manager client="client"></guac-file-transfer-manager>
</div>
<!-- Connection stability warning -->
<div id="connection-warning" ng-show="isConnectionUnstable()">
{{'CLIENT.TEXT_CLIENT_STATUS_UNSTABLE' | translate}}
</div>
<!-- Menu -->
<div class="menu" ng-class="{open: menu.shown}" id="guac-menu">
<div class="menu-content" ng-if="menu.shown">
<!-- Stationary header -->
<div class="header">
<h2 ng-hide="rootConnectionGroups">{{client.name}}</h2>
<h2 class="connection-select-menu" ng-show="rootConnectionGroups">
<guac-menu menu-title="client.name" interactive="true">
<div class="all-connections">
<guac-group-list-filter connection-groups="rootConnectionGroups"
filtered-connection-groups="filteredRootConnectionGroups"
placeholder="'CLIENT.FIELD_PLACEHOLDER_FILTER' | translate"
connection-properties="filteredConnectionProperties"
connection-group-properties="filteredConnectionGroupProperties"></guac-group-list-filter>
<guac-group-list
connection-groups="filteredRootConnectionGroups"
templates="{
'connection' : 'app/client/templates/connection.html',
'connection-group' : 'app/client/templates/connectionGroup.html'
}"
page-size="10"></guac-group-list>
</div>
</guac-menu>
</h2>
<div class="share-menu" ng-show="canShareConnection()">
<guac-menu menu-title="'CLIENT.ACTION_SHARE' | translate">
<ul ng-repeat="sharingProfile in sharingProfiles">
<li><a ng-click="share(sharingProfile)">{{sharingProfile.name}}</a></li>
</ul>
</guac-menu>
</div>
<guac-user-menu local-actions="clientMenuActions"></guac-user-menu>
</div>
<!-- Scrollable body -->
<div class="menu-body" guac-touch-drag="menuDrag" guac-scroll="menu.scrollState">
<!-- Connection sharing -->
<div class="menu-section" id="share-links" ng-show="isShared()">
<div class="content">
<h3>{{'CLIENT.INFO_CONNECTION_SHARED' | translate}}</h3>
<p class="description"
translate="CLIENT.HELP_SHARE_LINK"
translate-values="{LINKS : getShareLinkCount()}"></p>
<table>
<tr ng-repeat="link in client.shareLinks | toArray | orderBy: value.name">
<th>{{link.value.name}}</th>
<td><a href="{{link.value.href}}" target="_blank">{{link.value.href}}</a></td>
</tr>
</table>
</div>
</div>
<!-- Clipboard -->
<div class="menu-section" id="clipboard-settings">
<h3>{{'CLIENT.SECTION_HEADER_CLIPBOARD' | translate}}</h3>
<div class="content">
<p class="description">{{'CLIENT.HELP_CLIPBOARD' | translate}}</p>
<guac-clipboard data="client.clipboardData"></guac-clipboard>
</div>
</div>
<!-- Devices -->
<div class="menu-section" id="devices" ng-show="client.filesystems.length">
<h3>{{'CLIENT.SECTION_HEADER_DEVICES' | translate}}</h3>
<div class="content">
<div class="device filesystem" ng-repeat="filesystem in client.filesystems" ng-click="showFilesystemMenu(filesystem)">
{{filesystem.name}}
</div>
</div>
</div>
<!-- Connection parameters which may be modified while the connection is open -->
<div class="menu-section connection-parameters" id="connection-settings" ng-show="client.protocol">
<guac-form namespace="getProtocolNamespace(client.protocol)"
content="client.forms"
model="menu.connectionParameters"
model-only="true"></guac-form>
</div>
<!-- Input method -->
<div class="menu-section" id="keyboard-settings">
<h3>{{'CLIENT.SECTION_HEADER_INPUT_METHOD' | translate}}</h3>
<div class="content">
<!-- No IME -->
<div class="choice">
<label><input id="ime-none" name="input-method" ng-change="closeMenu()" ng-model="menu.inputMethod" type="radio" value="none"/> {{'CLIENT.NAME_INPUT_METHOD_NONE' | translate}}</label>
<p class="caption"><label for="ime-none">{{'CLIENT.HELP_INPUT_METHOD_NONE' | translate}}</label></p>
</div>
<!-- Text input -->
<div class="choice">
<div class="figure"><label for="ime-text"><img src="images/settings/tablet-keys.png" alt=""/></label></div>
<label><input id="ime-text" name="input-method" ng-change="closeMenu()" ng-model="menu.inputMethod" type="radio" value="text"/> {{'CLIENT.NAME_INPUT_METHOD_TEXT' | translate}}</label>
<p class="caption"><label for="ime-text">{{'CLIENT.HELP_INPUT_METHOD_TEXT' | translate}} </label></p>
</div>
<!-- Guac OSK -->
<div class="choice">
<label><input id="ime-osk" name="input-method" ng-change="closeMenu()" ng-model="menu.inputMethod" type="radio" value="osk"/> {{'CLIENT.NAME_INPUT_METHOD_OSK' | translate}}</label>
<p class="caption"><label for="ime-osk">{{'CLIENT.HELP_INPUT_METHOD_OSK' | translate}}</label></p>
</div>
</div>
</div>
<!-- Mouse mode -->
<div class="menu-section" id="mouse-settings" ng-hide="client.multiTouchSupport">
<h3>{{'CLIENT.SECTION_HEADER_MOUSE_MODE' | translate}}</h3>
<div class="content">
<p class="description">{{'CLIENT.HELP_MOUSE_MODE' | translate}}</p>
<!-- Touchscreen -->
<div class="choice">
<input name="mouse-mode" ng-change="closeMenu()" ng-model="client.clientProperties.emulateAbsoluteMouse" type="radio" ng-value="true" checked="checked" id="absolute"/>
<div class="figure">
<label for="absolute"><img src="images/settings/touchscreen.png" alt="{{'CLIENT.NAME_MOUSE_MODE_ABSOLUTE' | translate}}"/></label>
<p class="caption"><label for="absolute">{{'CLIENT.HELP_MOUSE_MODE_ABSOLUTE' | translate}}</label></p>
</div>
</div>
<!-- Touchpad -->
<div class="choice">
<input name="mouse-mode" ng-change="closeMenu()" ng-model="client.clientProperties.emulateAbsoluteMouse" type="radio" ng-value="false" id="relative"/>
<div class="figure">
<label for="relative"><img src="images/settings/touchpad.png" alt="{{'CLIENT.NAME_MOUSE_MODE_RELATIVE' | translate}}"/></label>
<p class="caption"><label for="relative">{{'CLIENT.HELP_MOUSE_MODE_RELATIVE' | translate}}</label></p>
</div>
</div>
</div>
</div>
<!-- Display options -->
<div class="menu-section" id="display-settings">
<h3>{{'CLIENT.SECTION_HEADER_DISPLAY' | translate}}</h3>
<div class="content">
<div id="zoom-settings">
<div ng-click="zoomOut()" id="zoom-out"><img src="images/settings/zoom-out.png" alt="-"/></div>
<div class="zoom-ctrl">
<input type="number" class="zoom-ctrl" guac-zoom-ctrl
ng-model="client.clientProperties.scale"
ng-model-options="{ updateOn: 'blur submit' }"
ng-change="zoomSet()" />%
</div>
<div ng-click="zoomIn()" id="zoom-in"><img src="images/settings/zoom-in.png" alt="+"/></div>
</div>
<div><label><input ng-model="menu.autoFit" ng-change="changeAutoFit()" ng-disabled="autoFitDisabled()" type="checkbox" id="auto-fit"/> {{'CLIENT.TEXT_ZOOM_AUTO_FIT' | translate}}</label></div>
</div>
</div>
</div>
</div>
</div>
<!-- Filesystem menu -->
<div id="filesystem-menu" class="menu" ng-class="{open: isFilesystemMenuShown()}">
<div class="menu-content">
<!-- Stationary header -->
<div class="header">
<h2>{{filesystemMenuContents.name}}</h2>
<button class="upload button" guac-upload="uploadFiles">{{'CLIENT.ACTION_UPLOAD_FILES' | translate}}</button>
<button class="back" ng-click="hideFilesystemMenu()">{{'CLIENT.ACTION_NAVIGATE_BACK' | translate}}</button>
</div>
<!-- Breadcrumbs -->
<div class="header breadcrumbs"><div
class="breadcrumb root"
ng-click="changeDirectory(filesystemMenuContents, filesystemMenuContents.root)"></div><div
class="breadcrumb"
ng-repeat="file in getPath(filesystemMenuContents.currentDirectory)"
ng-click="changeDirectory(filesystemMenuContents, file)">{{file.name}}</div>
</div>
<!-- Scrollable body -->
<div class="menu-body">
<guac-file-browser client="client" filesystem="filesystemMenuContents"></guac-file-browser>
</div>
</div>
</div>
</guac-viewport>

View File

@@ -0,0 +1,4 @@
<a class="connection" ng-href="{{ item.getClientURL() }}">
<div class="icon type" ng-class="item.protocol"></div>
<span class="name">{{item.name}}</span>
</a>

View File

@@ -0,0 +1,4 @@
<a class="connection-group" ng-href="{{ item.getClientURL() }}">
<div ng-show="item.balancing" class="icon type balancer"></div>
<span class="name">{{item.name}}</span>
</a>

View File

@@ -0,0 +1,9 @@
<div class="list-item">
<!-- Filename and icon -->
<div class="caption">
<div class="icon"></div>
{{::name}}
</div>
</div>

View File

@@ -0,0 +1,13 @@
<div class="main" guac-resize="mainElementResized">
<!-- Display -->
<div class="displayOuter">
<div class="displayMiddle">
<div class="display software-cursor">
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,32 @@
<div class="client-panel"
ng-class="{ 'has-clients': hasClients(), 'hidden' : panelHidden() }">
<!-- Toggle panel visibility -->
<div class="client-panel-handle" ng-click="togglePanel()"></div>
<!-- List of connection thumbnails -->
<ul class="client-panel-connection-list">
<li ng-repeat="client in clients | toArray | orderBy: [ '-value.lastUsed', 'value.title' ]"
ng-class="{ 'needs-attention' : hasStatusUpdate(client.value) }"
ng-show="isManaged(client.value)"
class="client-panel-connection">
<!-- Close connection -->
<button class="close-other-connection" ng-click="disconnect(client.value)">
<img ng-attr-alt="{{ 'CLIENT.ACTION_DISCONNECT' | translate }}"
ng-attr-title="{{ 'CLIENT.ACTION_DISCONNECT' | translate }}"
src="images/x.png">
</button>
<!-- Thumbnail -->
<a href="#/client/{{client.value.id}}">
<div class="thumbnail">
<guac-thumbnail client="client.value"></guac-thumbnail>
</div>
<div class="name">{{ client.value.title }}</div>
</a>
</li>
</ul>
</div>

View File

@@ -0,0 +1,6 @@
<div class="file-browser">
<!-- Current directory contents -->
<div class="current-directory-contents"></div>
</div>

View File

@@ -0,0 +1,22 @@
<div class="transfer" ng-class="{'in-progress': isInProgress(), 'savable': isSavable(), 'error': hasError()}" ng-click="save()">
<!-- Overall status of transfer -->
<div class="transfer-status">
<!-- Filename and progress bar -->
<div class="filename">
<div class="progress"><div ng-style="{'width': getPercentDone() + '%'}" class="bar"></div></div>
{{transfer.filename}}
</div>
<!-- Error text -->
<p class="error-text">{{getErrorText() | translate}}</p>
</div>
<!-- Progress/status text -->
<div class="text"
translate="CLIENT.TEXT_FILE_TRANSFER_PROGRESS"
translate-values="{PROGRESS: getProgressValue(), UNIT: getProgressUnit()}"></div>
</div>

View File

@@ -0,0 +1,22 @@
<div class="transfer-manager">
<!-- File transfer manager header -->
<div class="header">
<h2>{{'CLIENT.SECTION_HEADER_FILE_TRANSFERS' | translate}}</h2>
<button ng-click="clearCompletedTransfers()">{{'CLIENT.ACTION_CLEAR_COMPLETED_TRANSFERS' | translate}}</button>
</div>
<!-- Sent/received files -->
<div class="transfer-manager-body">
<div class="transfers">
<guac-file-transfer
transfer="upload"
ng-repeat="upload in client.uploads">
</guac-file-transfer><guac-file-transfer
transfer="download"
ng-repeat="download in client.downloads">
</guac-file-transfer>
</div>
</div>
</div>

View File

@@ -0,0 +1,10 @@
<div class="thumbnail-main" guac-resize="updateDisplayScale">
<!-- Display -->
<div class="display">
</div>
<!-- Dummy background thumbnail -->
<img alt="" ng-src="{{thumbnail}}"/>
</div>

View File

@@ -0,0 +1,2 @@
<div class="viewport" ng-transclude>
</div>

View File

@@ -0,0 +1,106 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A service for generating new guacClient properties objects.
*/
angular.module('client').factory('ClientProperties', ['$injector', function defineClientProperties($injector) {
// Required services
var preferenceService = $injector.get('preferenceService');
/**
* Object used for interacting with a guacClient directive.
*
* @constructor
* @param {ClientProperties|Object} [template={}]
* The object whose properties should be copied within the new
* ClientProperties.
*/
var ClientProperties = function ClientProperties(template) {
// Use empty object by default
template = template || {};
/**
* Whether the display should be scaled automatically to fit within the
* available space.
*
* @type Boolean
*/
this.autoFit = template.autoFit || true;
/**
* The current scale. If autoFit is true, the effect of setting this
* value is undefined.
*
* @type Number
*/
this.scale = template.scale || 1;
/**
* The minimum scale value.
*
* @type Number
*/
this.minScale = template.minScale || 1;
/**
* The maximum scale value.
*
* @type Number
*/
this.maxScale = template.maxScale || 3;
/**
* Whether or not the client should listen to keyboard events.
*
* @type Boolean
*/
this.keyboardEnabled = template.keyboardEnabled || true;
/**
* Whether translation of touch to mouse events should emulate an
* absolute pointer device, or a relative pointer device.
*
* @type Boolean
*/
this.emulateAbsoluteMouse = template.emulateAbsoluteMouse || preferenceService.preferences.emulateAbsoluteMouse;
/**
* The relative Y coordinate of the scroll offset of the display within
* the client element.
*
* @type Number
*/
this.scrollTop = template.scrollTop || 0;
/**
* The relative X coordinate of the scroll offset of the display within
* the client element.
*
* @type Number
*/
this.scrollLeft = template.scrollLeft || 0;
};
return ClientProperties;
}]);

View File

@@ -0,0 +1,152 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Provides the ManagedArgument class used by ManagedClient.
*/
angular.module('client').factory('ManagedArgument', ['$q', function defineManagedArgument($q) {
/**
* Object which represents an argument (connection parameter) which may be
* changed by the user while the connection is open.
*
* @constructor
* @param {ManagedArgument|Object} [template={}]
* The object whose properties should be copied within the new
* ManagedArgument.
*/
var ManagedArgument = function ManagedArgument(template) {
// Use empty object by default
template = template || {};
/**
* The name of the connection parameter.
*
* @type {String}
*/
this.name = template.name;
/**
* The current value of the connection parameter.
*
* @type {String}
*/
this.value = template.value;
/**
* A valid, open output stream which may be used to apply a new value
* to the connection parameter.
*
* @type {Guacamole.OutputStream}
*/
this.stream = template.stream;
};
/**
* Requests editable access to a given connection parameter, returning a
* promise which is resolved with a ManagedArgument instance that provides
* such access if the parameter is indeed editable.
*
* @param {ManagedClient} managedClient
* The ManagedClient instance associated with the connection for which
* an editable version of the connection parameter is being retrieved.
*
* @param {String} name
* The name of the connection parameter.
*
* @param {String} value
* The current value of the connection parameter, as received from a
* prior, inbound "argv" stream.
*
* @returns {Promise.<ManagedArgument>}
* A promise which is resolved with the new ManagedArgument instance
* once the requested parameter has been verified as editable.
*/
ManagedArgument.getInstance = function getInstance(managedClient, name, value) {
var deferred = $q.defer();
// Create internal, fully-populated instance of ManagedArgument, to be
// returned only once mutability of the associated connection parameter
// has been verified
var managedArgument = new ManagedArgument({
name : name,
value : value,
stream : managedClient.client.createArgumentValueStream('text/plain', name)
});
// The connection parameter is editable only if a successful "ack" is
// received
managedArgument.stream.onack = function ackReceived(status) {
if (status.isError())
deferred.reject(status);
else
deferred.resolve(managedArgument);
};
return deferred.promise;
};
/**
* Sets the given editable argument (connection parameter) to the given
* value, updating the behavior of the associated connection in real-time.
* If successful, the ManagedArgument provided cannot be used for future
* calls to setValue() and must be replaced with a new instance. This
* function only has an effect if the new parameter value is different from
* the current value.
*
* @param {ManagedArgument} managedArgument
* The ManagedArgument instance associated with the connection
* parameter being modified.
*
* @param {String} value
* The new value to assign to the connection parameter.
*
* @returns {Boolean}
* true if the connection parameter was sent and the provided
* ManagedArgument instance may no longer be used for future setValue()
* calls, false if the connection parameter was NOT sent as it has not
* changed.
*/
ManagedArgument.setValue = function setValue(managedArgument, value) {
// Stream new value only if value has changed
if (value !== managedArgument.value) {
var writer = new Guacamole.StringWriter(managedArgument.stream);
writer.sendText(value);
writer.sendEnd();
// ManagedArgument instance is no longer usable
return true;
}
// No parameter value change was attempted and the ManagedArgument
// instance may be reused
return false;
};
return ManagedArgument;
}]);

View File

@@ -0,0 +1,924 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Provides the ManagedClient class used by the guacClientManager service.
*/
angular.module('client').factory('ManagedClient', ['$rootScope', '$injector',
function defineManagedClient($rootScope, $injector) {
// Required types
var ClientProperties = $injector.get('ClientProperties');
var ClientIdentifier = $injector.get('ClientIdentifier');
var ClipboardData = $injector.get('ClipboardData');
var ManagedArgument = $injector.get('ManagedArgument');
var ManagedClientState = $injector.get('ManagedClientState');
var ManagedClientThumbnail = $injector.get('ManagedClientThumbnail');
var ManagedDisplay = $injector.get('ManagedDisplay');
var ManagedFilesystem = $injector.get('ManagedFilesystem');
var ManagedFileUpload = $injector.get('ManagedFileUpload');
var ManagedShareLink = $injector.get('ManagedShareLink');
// Required services
var $document = $injector.get('$document');
var $q = $injector.get('$q');
var $rootScope = $injector.get('$rootScope');
var $window = $injector.get('$window');
var activeConnectionService = $injector.get('activeConnectionService');
var authenticationService = $injector.get('authenticationService');
var connectionGroupService = $injector.get('connectionGroupService');
var connectionService = $injector.get('connectionService');
var preferenceService = $injector.get('preferenceService');
var requestService = $injector.get('requestService');
var schemaService = $injector.get('schemaService');
var tunnelService = $injector.get('tunnelService');
var guacAudio = $injector.get('guacAudio');
var guacHistory = $injector.get('guacHistory');
var guacImage = $injector.get('guacImage');
var guacVideo = $injector.get('guacVideo');
/**
* The minimum amount of time to wait between updates to the client
* thumbnail, in milliseconds.
*
* @type Number
*/
var THUMBNAIL_UPDATE_FREQUENCY = 5000;
/**
* Object which serves as a surrogate interface, encapsulating a Guacamole
* client while it is active, allowing it to be detached and reattached
* from different client views.
*
* @constructor
* @param {ManagedClient|Object} [template={}]
* The object whose properties should be copied within the new
* ManagedClient.
*/
var ManagedClient = function ManagedClient(template) {
// Use empty object by default
template = template || {};
/**
* The ID of the connection associated with this client.
*
* @type String
*/
this.id = template.id;
/**
* The time that the connection was last brought to the foreground of
* the current tab, as the number of milliseconds elapsed since
* midnight of January 1, 1970 UTC. If the connection has not yet been
* viewed, this will be 0.
*
* @type Number
*/
this.lastUsed = template.lastUsed || 0;
/**
* The actual underlying Guacamole client.
*
* @type Guacamole.Client
*/
this.client = template.client;
/**
* The tunnel being used by the underlying Guacamole client.
*
* @type Guacamole.Tunnel
*/
this.tunnel = template.tunnel;
/**
* The display associated with the underlying Guacamole client.
*
* @type ManagedDisplay
*/
this.managedDisplay = template.managedDisplay;
/**
* The name returned associated with the connection or connection
* group in use.
*
* @type String
*/
this.name = template.name;
/**
* The title which should be displayed as the page title for this
* client.
*
* @type String
*/
this.title = template.title;
/**
* The name which uniquely identifies the protocol of the connection in
* use. If the protocol cannot be determined, such as when a connection
* group is in use, this will be null.
*
* @type {String}
*/
this.protocol = template.protocol || null;
/**
* An array of forms describing all known parameters for the connection
* in use, including those which may not be editable.
*
* @type {Form[]}
*/
this.forms = template.forms || [];
/**
* The most recently-generated thumbnail for this connection, as
* stored within the local connection history. If no thumbnail is
* stored, this will be null.
*
* @type ManagedClientThumbnail
*/
this.thumbnail = template.thumbnail;
/**
* The current clipboard contents.
*
* @type ClipboardData
*/
this.clipboardData = template.clipboardData || new ClipboardData({
type : 'text/plain',
data : ''
});
/**
* The current state of all parameters requested by the server via
* "required" instructions, where each object key is the name of a
* requested parameter and each value is the current value entered by
* the user or null if no parameters are currently being requested.
*
* @type Object.<String, String>
*/
this.requiredParameters = null;
/**
* All uploaded files. As files are uploaded, their progress can be
* observed through the elements of this array. It is intended that
* this array be manipulated externally as needed.
*
* @type ManagedFileUpload[]
*/
this.uploads = template.uploads || [];
/**
* All currently-exposed filesystems. When the Guacamole server exposes
* a filesystem object, that object will be made available as a
* ManagedFilesystem within this array.
*
* @type ManagedFilesystem[]
*/
this.filesystems = template.filesystems || [];
/**
* All available share links generated for the this ManagedClient via
* ManagedClient.createShareLink(). Each resulting share link is stored
* under the identifier of its corresponding SharingProfile.
*
* @type Object.<String, ManagedShareLink>
*/
this.shareLinks = template.shareLinks || {};
/**
* The number of simultaneous touch contacts supported by the remote
* desktop. Unless explicitly declared otherwise by the remote desktop
* after connecting, this will be 0 (multi-touch unsupported).
*
* @type Number
*/
this.multiTouchSupport = template.multiTouchSupport || 0;
/**
* The current state of the Guacamole client (idle, connecting,
* connected, terminated with error, etc.).
*
* @type ManagedClientState
*/
this.clientState = template.clientState || new ManagedClientState();
/**
* Properties associated with the display and behavior of the Guacamole
* client.
*
* @type ClientProperties
*/
this.clientProperties = template.clientProperties || new ClientProperties();
/**
* All editable arguments (connection parameters), stored by their
* names. Arguments will only be present within this set if their
* current values have been exposed by the server via an inbound "argv"
* stream and the server has confirmed that the value may be changed
* through a successful "ack" to an outbound "argv" stream.
*
* @type {Object.<String, ManagedArgument>}
*/
this.arguments = template.arguments || {};
};
/**
* The mimetype of audio data to be sent along the Guacamole connection if
* audio input is supported.
*
* @constant
* @type String
*/
ManagedClient.AUDIO_INPUT_MIMETYPE = 'audio/L16;rate=44100,channels=2';
/**
* Returns a promise which resolves with the string of connection
* parameters to be passed to the Guacamole client during connection. This
* string generally contains the desired connection ID, display resolution,
* and supported audio/video/image formats. The returned promise is
* guaranteed to resolve successfully.
*
* @param {ClientIdentifier} identifier
* The identifier representing the connection or group to connect to.
*
* @param {String} [connectionParameters]
* Any additional HTTP parameters to pass while connecting.
*
* @returns {Promise.<String>}
* A promise which resolves with the string of connection parameters to
* be passed to the Guacamole client, once the string is ready.
*/
var getConnectString = function getConnectString(identifier, connectionParameters) {
var deferred = $q.defer();
// 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;
// Build base connect string
var connectString =
"token=" + encodeURIComponent(authenticationService.getCurrentToken())
+ "&GUAC_DATA_SOURCE=" + encodeURIComponent(identifier.dataSource)
+ "&GUAC_ID=" + encodeURIComponent(identifier.id)
+ "&GUAC_TYPE=" + encodeURIComponent(identifier.type)
+ "&GUAC_WIDTH=" + Math.floor(optimal_width)
+ "&GUAC_HEIGHT=" + Math.floor(optimal_height)
+ "&GUAC_DPI=" + Math.floor(optimal_dpi)
+ "&GUAC_TIMEZONE=" + encodeURIComponent(preferenceService.preferences.timezone)
+ (connectionParameters ? '&' + connectionParameters : '');
// Add audio mimetypes to connect string
guacAudio.supported.forEach(function(mimetype) {
connectString += "&GUAC_AUDIO=" + encodeURIComponent(mimetype);
});
// Add video mimetypes to connect string
guacVideo.supported.forEach(function(mimetype) {
connectString += "&GUAC_VIDEO=" + encodeURIComponent(mimetype);
});
// Add image mimetypes to connect string
guacImage.getSupportedMimetypes().then(function supportedMimetypesKnown(mimetypes) {
// Add each image mimetype
angular.forEach(mimetypes, function addImageMimetype(mimetype) {
connectString += "&GUAC_IMAGE=" + encodeURIComponent(mimetype);
});
// Connect string is now ready - nothing else is deferred
deferred.resolve(connectString);
});
return deferred.promise;
};
/**
* Requests the creation of a new audio stream, recorded from the user's
* local audio input device. If audio input is supported by the connection,
* an audio stream will be created which will remain open until the remote
* desktop requests that it be closed. If the audio stream is successfully
* created but is later closed, a new audio stream will automatically be
* established to take its place. The mimetype used for all audio streams
* produced by this function is defined by
* ManagedClient.AUDIO_INPUT_MIMETYPE.
*
* @param {Guacamole.Client} client
* The Guacamole.Client for which the audio stream is being requested.
*/
var requestAudioStream = function requestAudioStream(client) {
// Create new audio stream, associating it with an AudioRecorder
var stream = client.createAudioStream(ManagedClient.AUDIO_INPUT_MIMETYPE);
var recorder = Guacamole.AudioRecorder.getInstance(stream, ManagedClient.AUDIO_INPUT_MIMETYPE);
// If creation of the AudioRecorder failed, simply end the stream
if (!recorder)
stream.sendEnd();
// Otherwise, ensure that another audio stream is created after this
// audio stream is closed
else
recorder.onclose = requestAudioStream.bind(this, client);
};
/**
* Creates a new ManagedClient, connecting it to the specified connection
* or group.
*
* @param {String} id
* The ID of the connection or group to connect to. This String must be
* a valid ClientIdentifier string, as would be generated by
* ClientIdentifier.toString().
*
* @param {String} [connectionParameters]
* Any additional HTTP parameters to pass while connecting.
*
* @returns {ManagedClient}
* A new ManagedClient instance which is connected to the connection or
* connection group having the given ID.
*/
ManagedClient.getInstance = function getInstance(id, connectionParameters) {
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');
// Get new client instance
var client = new Guacamole.Client(tunnel);
// Associate new managed client with new client and tunnel
var managedClient = new ManagedClient({
id : id,
client : client,
tunnel : tunnel
});
// Fire events for tunnel errors
tunnel.onerror = function tunnelError(status) {
$rootScope.$apply(function handleTunnelError() {
ManagedClientState.setConnectionState(managedClient.clientState,
ManagedClientState.ConnectionState.TUNNEL_ERROR,
status.code);
});
};
// Pull protocol-specific information from tunnel once tunnel UUID is
// known
tunnel.onuuid = function tunnelAssignedUUID(uuid) {
tunnelService.getProtocol(uuid).then(function protocolRetrieved(protocol) {
managedClient.protocol = protocol.name;
managedClient.forms = protocol.connectionForms;
}, requestService.WARN);
};
// Update connection state as tunnel state changes
tunnel.onstatechange = function tunnelStateChanged(state) {
$rootScope.$evalAsync(function updateTunnelState() {
switch (state) {
// Connection is being established
case Guacamole.Tunnel.State.CONNECTING:
ManagedClientState.setConnectionState(managedClient.clientState,
ManagedClientState.ConnectionState.CONNECTING);
break;
// Connection is established / no longer unstable
case Guacamole.Tunnel.State.OPEN:
ManagedClientState.setTunnelUnstable(managedClient.clientState, false);
break;
// Connection is established but misbehaving
case Guacamole.Tunnel.State.UNSTABLE:
ManagedClientState.setTunnelUnstable(managedClient.clientState, true);
break;
// Connection has closed
case Guacamole.Tunnel.State.CLOSED:
ManagedClientState.setConnectionState(managedClient.clientState,
ManagedClientState.ConnectionState.DISCONNECTED);
break;
}
});
};
// Update connection state as client state changes
client.onstatechange = function clientStateChanged(clientState) {
$rootScope.$evalAsync(function updateClientState() {
switch (clientState) {
// Idle
case 0:
ManagedClientState.setConnectionState(managedClient.clientState,
ManagedClientState.ConnectionState.IDLE);
break;
// Ignore "connecting" state
case 1: // Connecting
break;
// Connected + waiting
case 2:
ManagedClientState.setConnectionState(managedClient.clientState,
ManagedClientState.ConnectionState.WAITING);
break;
// Connected
case 3:
ManagedClientState.setConnectionState(managedClient.clientState,
ManagedClientState.ConnectionState.CONNECTED);
// Send any clipboard data already provided
if (managedClient.clipboardData)
ManagedClient.setClipboard(managedClient, managedClient.clipboardData);
// Begin streaming audio input if possible
requestAudioStream(client);
// Update thumbnail with initial display contents
ManagedClient.updateThumbnail(managedClient);
break;
// Update history when disconnecting
case 4: // Disconnecting
case 5: // Disconnected
ManagedClient.updateThumbnail(managedClient);
break;
}
});
};
// Disconnect and update status when the client receives an error
client.onerror = function clientError(status) {
$rootScope.$apply(function handleClientError() {
// Disconnect, if connected
client.disconnect();
// Update state
ManagedClientState.setConnectionState(managedClient.clientState,
ManagedClientState.ConnectionState.CLIENT_ERROR,
status.code);
});
};
// Automatically update the client thumbnail
client.onsync = function syncReceived() {
var thumbnail = managedClient.thumbnail;
var timestamp = new Date().getTime();
// Update thumbnail if it doesn't exist or is old
if (!thumbnail || timestamp - thumbnail.timestamp >= THUMBNAIL_UPDATE_FREQUENCY) {
$rootScope.$apply(function updateClientThumbnail() {
ManagedClient.updateThumbnail(managedClient);
});
}
};
// Test for argument mutability whenever an argument value is
// received
client.onargv = function clientArgumentValueReceived(stream, mimetype, name) {
// Ignore arguments which do not use a mimetype currently supported
// by the web application
if (mimetype !== 'text/plain')
return;
var reader = new Guacamole.StringReader(stream);
// Assemble received data into a single string
var value = '';
reader.ontext = function textReceived(text) {
value += text;
};
// Test mutability once stream is finished, storing the current
// value for the argument only if it is mutable
reader.onend = function textComplete() {
ManagedArgument.getInstance(managedClient, name, value).then(function argumentIsMutable(argument) {
managedClient.arguments[name] = argument;
}, function ignoreImmutableArguments() {});
};
};
// Handle any received clipboard data
client.onclipboard = function clientClipboardReceived(stream, mimetype) {
var reader;
// If the received data is text, read it as a simple string
if (/^text\//.exec(mimetype)) {
reader = new Guacamole.StringReader(stream);
// Assemble received data into a single string
var data = '';
reader.ontext = function textReceived(text) {
data += text;
};
// Set clipboard contents once stream is finished
reader.onend = function textComplete() {
$rootScope.$apply(function updateClipboard() {
managedClient.clipboardData = new ClipboardData({
type : mimetype,
data : data
});
});
};
}
// Otherwise read the clipboard data as a Blob
else {
reader = new Guacamole.BlobReader(stream, mimetype);
reader.onend = function blobComplete() {
$rootScope.$apply(function updateClipboard() {
managedClient.clipboardData = new ClipboardData({
type : mimetype,
data : reader.getBlob()
});
});
};
}
};
// Update level of multi-touch support when known
client.onmultitouch = function multiTouchSupportDeclared(layer, touches) {
managedClient.multiTouchSupport = touches;
};
// Update title when a "name" instruction is received
client.onname = function clientNameReceived(name) {
$rootScope.$apply(function updateClientTitle() {
managedClient.title = name;
});
};
// Handle any received files
client.onfile = function clientFileReceived(stream, mimetype, filename) {
tunnelService.downloadStream(tunnel.uuid, stream, mimetype, filename);
};
// Handle any received filesystem objects
client.onfilesystem = function fileSystemReceived(object, name) {
$rootScope.$apply(function exposeFilesystem() {
managedClient.filesystems.push(ManagedFilesystem.getInstance(object, name));
});
};
// Handle any received prompts
client.onrequired = function onrequired(parameters) {
$rootScope.$apply(function promptUser() {
managedClient.requiredParameters = {};
angular.forEach(parameters, function populateParameter(name) {
managedClient.requiredParameters[name] = '';
});
});
};
// Manage the client display
managedClient.managedDisplay = ManagedDisplay.getInstance(client.getDisplay());
// Parse connection details from ID
var clientIdentifier = ClientIdentifier.fromString(id);
// Connect the Guacamole client
getConnectString(clientIdentifier, connectionParameters)
.then(function connectClient(connectString) {
client.connect(connectString);
});
// If using a connection, pull connection name and protocol information
if (clientIdentifier.type === ClientIdentifier.Types.CONNECTION) {
connectionService.getConnection(clientIdentifier.dataSource, clientIdentifier.id)
.then(function connectionRetrieved(connection) {
managedClient.name = managedClient.title = connection.name;
}, requestService.WARN);
}
// If using a connection group, pull connection name
else if (clientIdentifier.type === ClientIdentifier.Types.CONNECTION_GROUP) {
connectionGroupService.getConnectionGroup(clientIdentifier.dataSource, clientIdentifier.id)
.then(function connectionGroupRetrieved(group) {
managedClient.name = managedClient.title = group.name;
}, requestService.WARN);
}
// If using an active connection, pull corresponding connection, then
// pull connection name and protocol information from that
else if (clientIdentifier.type === ClientIdentifier.Types.ACTIVE_CONNECTION) {
activeConnectionService.getActiveConnection(clientIdentifier.dataSource, clientIdentifier.id)
.then(function activeConnectionRetrieved(activeConnection) {
// Attempt to retrieve connection details only if the
// underlying connection is known
if (activeConnection.connectionIdentifier) {
connectionService.getConnection(clientIdentifier.dataSource, activeConnection.connectionIdentifier)
.then(function connectionRetrieved(connection) {
managedClient.name = managedClient.title = connection.name;
}, requestService.WARN);
}
}, requestService.WARN);
}
return managedClient;
};
/**
* Uploads the given file to the server through the given Guacamole client.
* The file transfer can be monitored through the corresponding entry in
* the uploads array of the given managedClient.
*
* @param {ManagedClient} managedClient
* The ManagedClient through which the file is to be uploaded.
*
* @param {File} file
* The file to upload.
*
* @param {ManagedFilesystem} [filesystem]
* The filesystem to upload the file to, if any. If not specified, the
* file will be sent as a generic Guacamole file stream.
*
* @param {ManagedFilesystem.File} [directory=filesystem.currentDirectory]
* The directory within the given filesystem to upload the file to. If
* not specified, but a filesystem is given, the current directory of
* that filesystem will be used.
*/
ManagedClient.uploadFile = function uploadFile(managedClient, file, filesystem, directory) {
// Use generic Guacamole file streams by default
var object = null;
var streamName = null;
// If a filesystem is given, determine the destination object and stream
if (filesystem) {
object = filesystem.object;
streamName = (directory || filesystem.currentDirectory).streamName + '/' + file.name;
}
// Start and manage file upload
managedClient.uploads.push(ManagedFileUpload.getInstance(managedClient, file, object, streamName));
};
/**
* Sends the given clipboard data over the given Guacamole client, setting
* the contents of the remote clipboard to the data provided.
*
* @param {ManagedClient} managedClient
* The ManagedClient over which the given clipboard data is to be sent.
*
* @param {ClipboardData} data
* The clipboard data to send.
*/
ManagedClient.setClipboard = function setClipboard(managedClient, data) {
var writer;
// Create stream with proper mimetype
var stream = managedClient.client.createClipboardStream(data.type);
// Send data as a string if it is stored as a string
if (typeof data.data === 'string') {
writer = new Guacamole.StringWriter(stream);
writer.sendText(data.data);
writer.sendEnd();
}
// Otherwise, assume the data is a File/Blob
else {
// Write File/Blob asynchronously
writer = new Guacamole.BlobWriter(stream);
writer.oncomplete = function clipboardSent() {
writer.sendEnd();
};
// Begin sending data
writer.sendBlob(data.data);
}
};
/**
* Assigns the given value to the connection parameter having the given
* name, updating the behavior of the connection in real-time. If the
* connection parameter is not editable, this function has no effect.
*
* @param {ManagedClient} managedClient
* The ManagedClient instance associated with the active connection
* being modified.
*
* @param {String} name
* The name of the connection parameter to modify.
*
* @param {String} value
* The value to attempt to assign to the given connection parameter.
*/
ManagedClient.setArgument = function setArgument(managedClient, name, value) {
var managedArgument = managedClient.arguments[name];
if (managedArgument && ManagedArgument.setValue(managedArgument, value))
delete managedClient.arguments[name];
};
/**
* Sends the given connection parameter values using "argv" streams,
* updating the behavior of the connection in real-time if the server is
* expecting or requiring these parameters.
*
* @param {ManagedClient} managedClient
* The ManagedClient instance associated with the active connection
* being modified.
*
* @param {Object.<String, String>} values
* The set of values to attempt to assign to corresponding connection
* parameters, where each object key is the connection parameter being
* set.
*/
ManagedClient.sendArguments = function sendArguments(managedClient, values) {
angular.forEach(values, function sendArgument(value, name) {
var stream = managedClient.client.createArgumentValueStream("text/plain", name);
var writer = new Guacamole.StringWriter(stream);
writer.sendText(value);
writer.sendEnd();
});
};
/**
* Retrieves the current values of all editable connection parameters as a
* set of name/value pairs suitable for use as the model of a form which
* edits those parameters.
*
* @param {ManagedClient} client
* The ManagedClient instance associated with the active connection
* whose parameter values are being retrieved.
*
* @returns {Object.<String, String>}
* A new set of name/value pairs containing the current values of all
* editable parameters.
*/
ManagedClient.getArgumentModel = function getArgumentModel(client) {
var model = {};
angular.forEach(client.arguments, function addModelEntry(managedArgument) {
model[managedArgument.name] = managedArgument.value;
});
return model;
};
/**
* Produces a sharing link for the given ManagedClient using the given
* sharing profile. The resulting sharing link, and any required login
* information, can be retrieved from the <code>shareLinks</code> property
* of the given ManagedClient once the various underlying service calls
* succeed.
*
* @param {ManagedClient} client
* The ManagedClient which will be shared via the generated sharing
* link.
*
* @param {SharingProfile} sharingProfile
* The sharing profile to use to generate the sharing link.
*
* @returns {Promise}
* A Promise which is resolved once the sharing link has been
* successfully generated, and rejected if generating the link fails.
*/
ManagedClient.createShareLink = function createShareLink(client, sharingProfile) {
// Retrieve sharing credentials for the sake of generating a share link
var credentialRequest = tunnelService.getSharingCredentials(
client.tunnel.uuid, sharingProfile.identifier);
// Add a new share link once the credentials are ready
credentialRequest.then(function sharingCredentialsReceived(sharingCredentials) {
client.shareLinks[sharingProfile.identifier] =
ManagedShareLink.getInstance(sharingProfile, sharingCredentials);
}, requestService.WARN);
return credentialRequest;
};
/**
* Returns whether the given ManagedClient is being shared. A ManagedClient
* is shared if it has any associated share links.
*
* @param {ManagedClient} client
* The ManagedClient to check.
*
* @returns {Boolean}
* true if the ManagedClient has at least one associated share link,
* false otherwise.
*/
ManagedClient.isShared = function isShared(client) {
// The connection is shared if at least one share link exists
for (var dummy in client.shareLinks)
return true;
// No share links currently exist
return false;
};
/**
* Store the thumbnail of the given managed client within the connection
* history under its associated ID. If the client is not connected, this
* function has no effect.
*
* @param {ManagedClient} managedClient
* The client whose history entry should be updated.
*/
ManagedClient.updateThumbnail = function updateThumbnail(managedClient) {
var display = managedClient.client.getDisplay();
// Update stored thumbnail of previous connection
if (display && display.getWidth() > 0 && display.getHeight() > 0) {
// Get screenshot
var canvas = display.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[0].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
);
// Store updated thumbnail within client
managedClient.thumbnail = new ManagedClientThumbnail({
timestamp : new Date().getTime(),
canvas : thumbnail
});
// Update historical thumbnail
guacHistory.updateThumbnail(managedClient.id, thumbnail.toDataURL("image/png"));
}
};
return ManagedClient;
}]);

View File

@@ -0,0 +1,185 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Provides the ManagedClient class used by the guacClientManager service.
*/
angular.module('client').factory('ManagedClientState', [function defineManagedClientState() {
/**
* Object which represents the state of a Guacamole client and its tunnel,
* including any error conditions.
*
* @constructor
* @param {ManagedClientState|Object} [template={}]
* The object whose properties should be copied within the new
* ManagedClientState.
*/
var ManagedClientState = function ManagedClientState(template) {
// Use empty object by default
template = template || {};
/**
* The current connection state. Valid values are described by
* ManagedClientState.ConnectionState.
*
* @type String
* @default ManagedClientState.ConnectionState.IDLE
*/
this.connectionState = template.connectionState || ManagedClientState.ConnectionState.IDLE;
/**
* Whether the network connection used by the tunnel seems unstable. If
* the network connection is unstable, the remote desktop connection
* may perform poorly or disconnect.
*
* @type Boolean
* @default false
*/
this.tunnelUnstable = template.tunnelUnstable || false;
/**
* The status code of the current error condition, if connectionState
* is CLIENT_ERROR or TUNNEL_ERROR. For all other connectionState
* values, this will be @link{Guacamole.Status.Code.SUCCESS}.
*
* @type Number
* @default Guacamole.Status.Code.SUCCESS
*/
this.statusCode = template.statusCode || Guacamole.Status.Code.SUCCESS;
};
/**
* Valid connection state strings. Each state string is associated with a
* specific state of a Guacamole connection.
*/
ManagedClientState.ConnectionState = {
/**
* The Guacamole connection has not yet been attempted.
*
* @type String
*/
IDLE : "IDLE",
/**
* The Guacamole connection is being established.
*
* @type String
*/
CONNECTING : "CONNECTING",
/**
* The Guacamole connection has been successfully established, and the
* client is now waiting for receipt of initial graphical data.
*
* @type String
*/
WAITING : "WAITING",
/**
* The Guacamole connection has been successfully established, and
* initial graphical data has been received.
*
* @type String
*/
CONNECTED : "CONNECTED",
/**
* The Guacamole connection has terminated successfully. No errors are
* indicated.
*
* @type String
*/
DISCONNECTED : "DISCONNECTED",
/**
* The Guacamole connection has terminated due to an error reported by
* the client. The associated error code is stored in statusCode.
*
* @type String
*/
CLIENT_ERROR : "CLIENT_ERROR",
/**
* The Guacamole connection has terminated due to an error reported by
* the tunnel. The associated error code is stored in statusCode.
*
* @type String
*/
TUNNEL_ERROR : "TUNNEL_ERROR"
};
/**
* Sets the current client state and, if given, the associated status code.
* If an error is already represented, this function has no effect. If the
* client state was previously marked as unstable, that flag is implicitly
* cleared.
*
* @param {ManagedClientState} clientState
* The ManagedClientState to update.
*
* @param {String} connectionState
* The connection state to assign to the given ManagedClientState, as
* listed within ManagedClientState.ConnectionState.
*
* @param {Number} [statusCode]
* The status code to assign to the given ManagedClientState, if any,
* as listed within Guacamole.Status.Code. If no status code is
* specified, the status code of the ManagedClientState is not touched.
*/
ManagedClientState.setConnectionState = function(clientState, connectionState, statusCode) {
// Do not set state after an error is registered
if (clientState.connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR
|| clientState.connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR)
return;
// Update connection state
clientState.connectionState = connectionState;
clientState.tunnelUnstable = false;
// Set status code, if given
if (statusCode)
clientState.statusCode = statusCode;
};
/**
* Updates the given client state, setting whether the underlying tunnel
* is currently unstable. An unstable tunnel is not necessarily
* disconnected, but appears to be misbehaving and may be disconnected.
*
* @param {ManagedClientState} clientState
* The ManagedClientState to update.
*
* @param {Boolean} unstable
* Whether the underlying tunnel of the connection currently appears
* unstable.
*/
ManagedClientState.setTunnelUnstable = function setTunnelUnstable(clientState, unstable) {
clientState.tunnelUnstable = unstable;
};
return ManagedClientState;
}]);

View File

@@ -0,0 +1,58 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Provides the ManagedClientThumbnail class used by ManagedClient.
*/
angular.module('client').factory('ManagedClientThumbnail', [function defineManagedClientThumbnail() {
/**
* Object which represents a thumbnail of the Guacamole client display,
* along with the time that the thumbnail was generated.
*
* @constructor
* @param {ManagedClientThumbnail|Object} [template={}]
* The object whose properties should be copied within the new
* ManagedClientThumbnail.
*/
var ManagedClientThumbnail = function ManagedClientThumbnail(template) {
// Use empty object by default
template = template || {};
/**
* The time that this thumbnail was generated, as the number of
* milliseconds elapsed since midnight of January 1, 1970 UTC.
*
* @type Number
*/
this.timestamp = template.timestamp;
/**
* The thumbnail of the Guacamole client display.
*
* @type HTMLCanvasElement
*/
this.canvas = template.canvas;
};
return ManagedClientThumbnail;
}]);

View File

@@ -0,0 +1,174 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Provides the ManagedDisplay class used by the guacClientManager service.
*/
angular.module('client').factory('ManagedDisplay', ['$rootScope',
function defineManagedDisplay($rootScope) {
/**
* Object which serves as a surrogate interface, encapsulating a Guacamole
* display while it is active, allowing it to be detached and reattached
* from different client views.
*
* @constructor
* @param {ManagedDisplay|Object} [template={}]
* The object whose properties should be copied within the new
* ManagedDisplay.
*/
var ManagedDisplay = function ManagedDisplay(template) {
// Use empty object by default
template = template || {};
/**
* The underlying Guacamole display.
*
* @type Guacamole.Display
*/
this.display = template.display;
/**
* The current size of the Guacamole display.
*
* @type ManagedDisplay.Dimensions
*/
this.size = new ManagedDisplay.Dimensions(template.size);
/**
* The current mouse cursor, if any.
*
* @type ManagedDisplay.Cursor
*/
this.cursor = template.cursor;
};
/**
* Object which represents the size of the Guacamole display.
*
* @constructor
* @param {ManagedDisplay.Dimensions|Object} template
* The object whose properties should be copied within the new
* ManagedDisplay.Dimensions.
*/
ManagedDisplay.Dimensions = function Dimensions(template) {
// Use empty object by default
template = template || {};
/**
* The current width of the Guacamole display, in pixels.
*
* @type Number
*/
this.width = template.width || 0;
/**
* The current width of the Guacamole display, in pixels.
*
* @type Number
*/
this.height = template.height || 0;
};
/**
* Object which represents a mouse cursor used by the Guacamole display.
*
* @constructor
* @param {ManagedDisplay.Cursor|Object} template
* The object whose properties should be copied within the new
* ManagedDisplay.Cursor.
*/
ManagedDisplay.Cursor = function Cursor(template) {
// Use empty object by default
template = template || {};
/**
* The actual mouse cursor image.
*
* @type HTMLCanvasElement
*/
this.canvas = template.canvas;
/**
* The X coordinate of the cursor hotspot.
*
* @type Number
*/
this.x = template.x;
/**
* The Y coordinate of the cursor hotspot.
*
* @type Number
*/
this.y = template.y;
};
/**
* Creates a new ManagedDisplay which represents the current state of the
* given Guacamole display.
*
* @param {Guacamole.Display} display
* The Guacamole display to represent. Changes to this display will
* affect this ManagedDisplay.
*
* @returns {ManagedDisplay}
* A new ManagedDisplay which represents the current state of the
* given Guacamole display.
*/
ManagedDisplay.getInstance = function getInstance(display) {
var managedDisplay = new ManagedDisplay({
display : display
});
// Store changes to display size
display.onresize = function setClientSize() {
$rootScope.$apply(function updateClientSize() {
managedDisplay.size = new ManagedDisplay.Dimensions({
width : display.getWidth(),
height : display.getHeight()
});
});
};
// Store changes to display cursor
display.oncursor = function setClientCursor(canvas, x, y) {
$rootScope.$apply(function updateClientCursor() {
managedDisplay.cursor = new ManagedDisplay.Cursor({
canvas : canvas,
x : x,
y : y
});
});
};
return managedDisplay;
};
return ManagedDisplay;
}]);

View File

@@ -0,0 +1,133 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Provides the ManagedFileTransferState class used by the guacClientManager
* service.
*/
angular.module('client').factory('ManagedFileTransferState', [function defineManagedFileTransferState() {
/**
* Object which represents the state of a Guacamole stream, including any
* error conditions.
*
* @constructor
* @param {ManagedFileTransferState|Object} [template={}]
* The object whose properties should be copied within the new
* ManagedFileTransferState.
*/
var ManagedFileTransferState = function ManagedFileTransferState(template) {
// Use empty object by default
template = template || {};
/**
* The current stream state. Valid values are described by
* ManagedFileTransferState.StreamState.
*
* @type String
* @default ManagedFileTransferState.StreamState.IDLE
*/
this.streamState = template.streamState || ManagedFileTransferState.StreamState.IDLE;
/**
* The status code of the current error condition, if streamState
* is ERROR. For all other streamState values, this will be
* @link{Guacamole.Status.Code.SUCCESS}.
*
* @type Number
* @default Guacamole.Status.Code.SUCCESS
*/
this.statusCode = template.statusCode || Guacamole.Status.Code.SUCCESS;
};
/**
* Valid stream state strings. Each state string is associated with a
* specific state of a Guacamole stream.
*/
ManagedFileTransferState.StreamState = {
/**
* The stream has not yet been opened.
*
* @type String
*/
IDLE : "IDLE",
/**
* The stream has been successfully established. Data can be sent or
* received.
*
* @type String
*/
OPEN : "OPEN",
/**
* The stream has terminated successfully. No errors are indicated.
*
* @type String
*/
CLOSED : "CLOSED",
/**
* The stream has terminated due to an error. The associated error code
* is stored in statusCode.
*
* @type String
*/
ERROR : "ERROR"
};
/**
* Sets the current transfer state and, if given, the associated status
* code. If an error is already represented, this function has no effect.
*
* @param {ManagedFileTransferState} transferState
* The ManagedFileTransferState to update.
*
* @param {String} streamState
* The stream state to assign to the given ManagedFileTransferState, as
* listed within ManagedFileTransferState.StreamState.
*
* @param {Number} [statusCode]
* The status code to assign to the given ManagedFileTransferState, if
* any, as listed within Guacamole.Status.Code. If no status code is
* specified, the status code of the ManagedFileTransferState is not
* touched.
*/
ManagedFileTransferState.setStreamState = function setStreamState(transferState, streamState, statusCode) {
// Do not set state after an error is registered
if (transferState.streamState === ManagedFileTransferState.StreamState.ERROR)
return;
// Update stream state
transferState.streamState = streamState;
// Set status code, if given
if (statusCode)
transferState.statusCode = statusCode;
};
return ManagedFileTransferState;
}]);

View File

@@ -0,0 +1,202 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Provides the ManagedFileUpload class used by the guacClientManager service.
*/
angular.module('client').factory('ManagedFileUpload', ['$rootScope', '$injector',
function defineManagedFileUpload($rootScope, $injector) {
// Required types
var Error = $injector.get('Error');
var ManagedFileTransferState = $injector.get('ManagedFileTransferState');
// Required services
var requestService = $injector.get('requestService');
var tunnelService = $injector.get('tunnelService');
/**
* Object which serves as a surrogate interface, encapsulating a Guacamole
* file upload while it is active, allowing it to be detached and
* reattached from different client views.
*
* @constructor
* @param {ManagedFileUpload|Object} [template={}]
* The object whose properties should be copied within the new
* ManagedFileUpload.
*/
var ManagedFileUpload = function ManagedFileUpload(template) {
// Use empty object by default
template = template || {};
/**
* The current state of the file transfer stream.
*
* @type ManagedFileTransferState
*/
this.transferState = template.transferState || new ManagedFileTransferState();
/**
* The mimetype of the file being transferred.
*
* @type String
*/
this.mimetype = template.mimetype;
/**
* The filename of the file being transferred.
*
* @type String
*/
this.filename = template.filename;
/**
* The number of bytes transferred so far.
*
* @type Number
*/
this.progress = template.progress;
/**
* The total number of bytes in the file.
*
* @type Number
*/
this.length = template.length;
};
/**
* Creates a new ManagedFileUpload which uploads the given file to the
* server through the given Guacamole client.
*
* @param {ManagedClient} managedClient
* The ManagedClient through which the file is to be uploaded.
*
* @param {File} file
* The file to upload.
*
* @param {Object} [object]
* The object to upload the file to, if any, such as a filesystem
* object.
*
* @param {String} [streamName]
* The name of the stream to upload the file to. If an object is given,
* this must be specified.
*
* @return {ManagedFileUpload}
* A new ManagedFileUpload object which can be used to track the
* progress of the upload.
*/
ManagedFileUpload.getInstance = function getInstance(managedClient, file, object, streamName) {
var managedFileUpload = new ManagedFileUpload();
// Pull Guacamole.Tunnel and Guacamole.Client from given ManagedClient
var client = managedClient.client;
var tunnel = managedClient.tunnel;
// Open file for writing
var stream;
if (!object)
stream = client.createFileStream(file.type, file.name);
// If object/streamName specified, upload to that instead of a file
// stream
else
stream = object.createOutputStream(file.type, streamName);
// Notify that the file transfer is pending
$rootScope.$evalAsync(function uploadStreamOpen() {
// Init managed upload
managedFileUpload.filename = file.name;
managedFileUpload.mimetype = file.type;
managedFileUpload.progress = 0;
managedFileUpload.length = file.size;
// Notify that stream is open
ManagedFileTransferState.setStreamState(managedFileUpload.transferState,
ManagedFileTransferState.StreamState.OPEN);
});
// Upload file once stream is acknowledged
stream.onack = function beginUpload(status) {
// Notify of any errors from the Guacamole server
if (status.isError()) {
$rootScope.$apply(function uploadStreamError() {
ManagedFileTransferState.setStreamState(managedFileUpload.transferState,
ManagedFileTransferState.StreamState.ERROR,
status.code);
});
return;
}
// Begin upload
tunnelService.uploadToStream(tunnel.uuid, stream, file, function uploadContinuing(length) {
$rootScope.$apply(function uploadStreamProgress() {
managedFileUpload.progress = length;
});
})
// Notify if upload succeeds
.then(function uploadSuccessful() {
// Upload complete
managedFileUpload.progress = file.size;
ManagedFileTransferState.setStreamState(managedFileUpload.transferState,
ManagedFileTransferState.StreamState.CLOSED);
// Notify of upload completion
$rootScope.$broadcast('guacUploadComplete', file.name);
},
// Notify if upload fails
requestService.createErrorCallback(function uploadFailed(error) {
// Use provide status code if the error is coming from the stream
if (error.type === Error.Type.STREAM_ERROR)
ManagedFileTransferState.setStreamState(managedFileUpload.transferState,
ManagedFileTransferState.StreamState.ERROR,
error.statusCode);
// Fail with internal error for all other causes
else
ManagedFileTransferState.setStreamState(managedFileUpload.transferState,
ManagedFileTransferState.StreamState.ERROR,
Guacamole.Status.Code.INTERNAL_ERROR);
}));
// Ignore all further acks
stream.onack = null;
};
return managedFileUpload;
};
return ManagedFileUpload;
}]);

View File

@@ -0,0 +1,330 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Provides the ManagedFilesystem class used by ManagedClient to represent
* available remote filesystems.
*/
angular.module('client').factory('ManagedFilesystem', ['$rootScope', '$injector',
function defineManagedFilesystem($rootScope, $injector) {
// Required types
var tunnelService = $injector.get('tunnelService');
/**
* Object which serves as a surrogate interface, encapsulating a Guacamole
* filesystem object while it is active, allowing it to be detached and
* reattached from different client views.
*
* @constructor
* @param {ManagedFilesystem|Object} [template={}]
* The object whose properties should be copied within the new
* ManagedFilesystem.
*/
var ManagedFilesystem = function ManagedFilesystem(template) {
// Use empty object by default
template = template || {};
/**
* The Guacamole filesystem object, as received via a "filesystem"
* instruction.
*
* @type Guacamole.Object
*/
this.object = template.object;
/**
* The declared, human-readable name of the filesystem
*
* @type String
*/
this.name = template.name;
/**
* The root directory of the filesystem.
*
* @type ManagedFilesystem.File
*/
this.root = template.root;
/**
* The current directory being viewed or manipulated within the
* filesystem.
*
* @type ManagedFilesystem.File
*/
this.currentDirectory = template.currentDirectory || template.root;
};
/**
* Refreshes the contents of the given file, if that file is a directory.
* Only the immediate children of the file are refreshed. Files further
* down the directory tree are not refreshed.
*
* @param {ManagedFilesystem} filesystem
* The filesystem associated with the file being refreshed.
*
* @param {ManagedFilesystem.File} file
* The file being refreshed.
*/
ManagedFilesystem.refresh = function updateDirectory(filesystem, file) {
// Do not attempt to refresh the contents of directories
if (file.mimetype !== Guacamole.Object.STREAM_INDEX_MIMETYPE)
return;
// Request contents of given file
filesystem.object.requestInputStream(file.streamName, function handleStream(stream, mimetype) {
// Ignore stream if mimetype is wrong
if (mimetype !== Guacamole.Object.STREAM_INDEX_MIMETYPE) {
stream.sendAck('Unexpected mimetype', Guacamole.Status.Code.UNSUPPORTED);
return;
}
// Signal server that data is ready to be received
stream.sendAck('Ready', Guacamole.Status.Code.SUCCESS);
// Read stream as JSON
var reader = new Guacamole.JSONReader(stream);
// Acknowledge received JSON blobs
reader.onprogress = function onprogress() {
stream.sendAck("Received", Guacamole.Status.Code.SUCCESS);
};
// Reset contents of directory
reader.onend = function jsonReady() {
$rootScope.$evalAsync(function updateFileContents() {
// Empty contents
file.files = {};
// Determine the expected filename prefix of each stream
var expectedPrefix = file.streamName;
if (expectedPrefix.charAt(expectedPrefix.length - 1) !== '/')
expectedPrefix += '/';
// For each received stream name
var mimetypes = reader.getJSON();
for (var name in mimetypes) {
// Assert prefix is correct
if (name.substring(0, expectedPrefix.length) !== expectedPrefix)
continue;
// Extract filename from stream name
var filename = name.substring(expectedPrefix.length);
// Deduce type from mimetype
var type = ManagedFilesystem.File.Type.NORMAL;
if (mimetypes[name] === Guacamole.Object.STREAM_INDEX_MIMETYPE)
type = ManagedFilesystem.File.Type.DIRECTORY;
// Add file entry
file.files[filename] = new ManagedFilesystem.File({
mimetype : mimetypes[name],
streamName : name,
type : type,
parent : file,
name : filename
});
}
});
};
});
};
/**
* Creates a new ManagedFilesystem instance from the given Guacamole.Object
* and human-readable name. Upon creation, a request to populate the
* contents of the root directory will be automatically dispatched.
*
* @param {Guacamole.Object} object
* The Guacamole.Object defining the filesystem.
*
* @param {String} name
* A human-readable name for the filesystem.
*
* @returns {ManagedFilesystem}
* The newly-created ManagedFilesystem.
*/
ManagedFilesystem.getInstance = function getInstance(object, name) {
// Init new filesystem object
var managedFilesystem = new ManagedFilesystem({
object : object,
name : name,
root : new ManagedFilesystem.File({
mimetype : Guacamole.Object.STREAM_INDEX_MIMETYPE,
streamName : Guacamole.Object.ROOT_STREAM,
type : ManagedFilesystem.File.Type.DIRECTORY
})
});
// Retrieve contents of root
ManagedFilesystem.refresh(managedFilesystem, managedFilesystem.root);
return managedFilesystem;
};
/**
* Downloads the given file from the server using the given Guacamole
* client and filesystem. The browser will automatically start the
* download upon completion of this function.
*
* @param {ManagedClient} managedClient
* The ManagedClient from which the file is to be downloaded.
*
* @param {ManagedFilesystem} managedFilesystem
* The ManagedFilesystem from which the file is to be downloaded. Any
* path information provided must be relative to this filesystem.
*
* @param {String} path
* The full, absolute path of the file to download.
*/
ManagedFilesystem.downloadFile = function downloadFile(managedClient, managedFilesystem, path) {
// Request download
managedFilesystem.object.requestInputStream(path, function downloadStreamReceived(stream, mimetype) {
// Parse filename from string
var filename = path.match(/(.*[\\/])?(.*)/)[2];
// Start download
tunnelService.downloadStream(managedClient.tunnel.uuid, stream, mimetype, filename);
});
};
/**
* Changes the current directory of the given filesystem, automatically
* refreshing the contents of that directory.
*
* @param {ManagedFilesystem} filesystem
* The filesystem whose current directory should be changed.
*
* @param {ManagedFilesystem.File} file
* The directory to change to.
*/
ManagedFilesystem.changeDirectory = function changeDirectory(filesystem, file) {
// Refresh contents
ManagedFilesystem.refresh(filesystem, file);
// Set current directory
filesystem.currentDirectory = file;
};
/**
* A file within a ManagedFilesystem. Each ManagedFilesystem.File provides
* sufficient information for retrieval or replacement of the file's
* contents, as well as the file's name and type.
*
* @param {ManagedFilesystem|Object} [template={}]
* The object whose properties should be copied within the new
* ManagedFilesystem.File.
*/
ManagedFilesystem.File = function File(template) {
/**
* The mimetype of the data contained within this file.
*
* @type String
*/
this.mimetype = template.mimetype;
/**
* The name of the stream representing this files contents within its
* associated filesystem object.
*
* @type String
*/
this.streamName = template.streamName;
/**
* The type of this file. All legal file type strings are defined
* within ManagedFilesystem.File.Type.
*
* @type String
*/
this.type = template.type;
/**
* The name of this file.
*
* @type String
*/
this.name = template.name;
/**
* The parent directory of this file. In the case of the root
* directory, this will be null.
*
* @type ManagedFilesystem.File
*/
this.parent = template.parent;
/**
* Map of all known files containined within this file by name. This is
* only applicable to directories.
*
* @type Object.<String, ManagedFilesystem.File>
*/
this.files = template.files || {};
};
/**
* All legal type strings for a ManagedFilesystem.File.
*
* @type Object.<String, String>
*/
ManagedFilesystem.File.Type = {
/**
* A normal file. As ManagedFilesystem does not currently represent any
* other non-directory types of files, like symbolic links, this type
* string may be used for any non-directory file.
*
* @type String
*/
NORMAL : 'NORMAL',
/**
* A directory.
*
* @type String
*/
DIRECTORY : 'DIRECTORY'
};
return ManagedFilesystem;
}]);

View File

@@ -0,0 +1,105 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Provides the ManagedShareLink class used by ManagedClient to represent
* generated connection sharing links.
*/
angular.module('client').factory('ManagedShareLink', ['$injector',
function defineManagedShareLink($injector) {
// Required types
var UserCredentials = $injector.get('UserCredentials');
/**
* Object which represents a link which can be used to gain access to an
* active Guacamole connection.
*
* @constructor
* @param {ManagedShareLink|Object} [template={}]
* The object whose properties should be copied within the new
* ManagedShareLink.
*/
var ManagedShareLink = function ManagedShareLink(template) {
// Use empty object by default
template = template || {};
/**
* The human-readable display name of this share link.
*
* @type String
*/
this.name = template.name;
/**
* The actual URL of the link which can be used to access the shared
* connection.
*
* @type String
*/
this.href = template.href;
/**
* The sharing profile which was used to generate the share link.
*
* @type SharingProfile
*/
this.sharingProfile = template.sharingProfile;
/**
* The credentials from which the share link was derived.
*
* @type UserCredentials
*/
this.sharingCredentials = template.sharingCredentials;
};
/**
* Creates a new ManagedShareLink from a set of UserCredentials and the
* SharingProfile which was used to generate those UserCredentials.
*
* @param {SharingProfile} sharingProfile
* The SharingProfile which was used, via the REST API, to generate the
* given UserCredentials.
*
* @param {UserCredentials} sharingCredentials
* The UserCredentials object returned by the REST API in response to a
* request to share a connection using the given SharingProfile.
*
* @return {ManagedShareLink}
* A new ManagedShareLink object can be used to access the connection
* shared via the given SharingProfile and resulting UserCredentials.
*/
ManagedShareLink.getInstance = function getInstance(sharingProfile, sharingCredentials) {
// Generate new share link using the given profile and credentials
return new ManagedShareLink({
'name' : sharingProfile.name,
'href' : UserCredentials.getLink(sharingCredentials),
'sharingProfile' : sharingProfile,
'sharingCredentials' : sharingCredentials
});
};
return ManagedShareLink;
}]);

View File

@@ -0,0 +1,23 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* The module for code used to manipulate/observe the clipboard.
*/
angular.module('clipboard', []);

View File

@@ -0,0 +1,108 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive provides an editor whose contents are exposed via a
* ClipboardData object via the "data" attribute. If this data should also be
* synced to the local clipboard, or sent via a connected Guacamole client
* using a "guacClipboard" event, it is up to external code to do so.
*/
angular.module('clipboard').directive('guacClipboard', ['$injector',
function guacClipboard($injector) {
// Required types
var ClipboardData = $injector.get('ClipboardData');
/**
* Configuration object for the guacClipboard directive.
*
* @type Object.<String, Object>
*/
var config = {
restrict : 'E',
replace : true,
templateUrl : 'app/clipboard/templates/guacClipboard.html'
};
// Scope properties exposed by the guacClipboard directive
config.scope = {
/**
* The data to display within the field provided by this directive. This
* data will modified or replaced when the user manually alters the
* contents of the field.
*
* @type ClipboardData
*/
data : '='
};
// guacClipboard directive controller
config.controller = ['$scope', '$injector', '$element',
function guacClipboardController($scope, $injector, $element) {
/**
* The DOM element which will contain the clipboard contents within the
* user interface provided by this directive.
*
* @type Element
*/
var element = $element[0];
/**
* Rereads the contents of the clipboard field, updating the
* ClipboardData object on the scope as necessary. The type of data
* stored within the ClipboardData object will be heuristically
* determined from the HTML contents of the clipboard field.
*/
var updateClipboardData = function updateClipboardData() {
// Read contents of clipboard textarea
$scope.$evalAsync(function assignClipboardText() {
$scope.data = new ClipboardData({
type : 'text/plain',
data : element.value
});
});
};
// Update the internally-stored clipboard data when events are fired
// that indicate the clipboard field may have been changed
element.addEventListener('input', updateClipboardData);
element.addEventListener('change', updateClipboardData);
// Watch clipboard for new data, updating the clipboard textarea as
// necessary
$scope.$watch('data', function clipboardDataChanged(data) {
// If the clipboard data is a string, render it as text
if (typeof data.data === 'string')
element.value = data.data;
// Ignore other data types for now
}); // end $scope.data watch
}];
return config;
}]);

View File

@@ -0,0 +1,553 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A service for accessing local clipboard data.
*/
angular.module('clipboard').factory('clipboardService', ['$injector',
function clipboardService($injector) {
// Get required services
var $q = $injector.get('$q');
var $window = $injector.get('$window');
// Required types
var ClipboardData = $injector.get('ClipboardData');
var service = {};
/**
* The amount of time to wait before actually serving a request to read
* clipboard data, in milliseconds. Providing a reasonable delay between
* request and read attempt allows the cut/copy operation to settle, in
* case the data we are anticipating to be present is not actually present
* in the clipboard yet.
*
* @constant
* @type Number
*/
var CLIPBOARD_READ_DELAY = 100;
/**
* The promise associated with the current pending clipboard read attempt.
* If no clipboard read is active, this will be null.
*
* @type Promise.<ClipboardData>
*/
var pendingRead = null;
/**
* Reference to the window.document object.
*
* @private
* @type HTMLDocument
*/
var document = $window.document;
/**
* The textarea that will be used to hold the local clipboard contents.
*
* @type Element
*/
var clipboardContent = document.createElement('textarea');
// Ensure clipboard target is selectable but not visible
clipboardContent.className = 'clipboard-service-target';
// Add clipboard target to DOM
document.body.appendChild(clipboardContent);
/**
* Stops the propogation of the given event through the DOM tree. This is
* identical to invoking stopPropogation() on the event directly, except
* that this function is usable as an event handler itself.
*
* @param {Event} e
* The event whose propogation through the DOM tree should be stopped.
*/
var stopEventPropagation = function stopEventPropagation(e) {
e.stopPropagation();
};
// Prevent events generated due to execCommand() from disturbing external things
clipboardContent.addEventListener('cut', stopEventPropagation);
clipboardContent.addEventListener('copy', stopEventPropagation);
clipboardContent.addEventListener('paste', stopEventPropagation);
clipboardContent.addEventListener('input', stopEventPropagation);
/**
* A stack of past node selection ranges. A range convering the nodes
* currently selected within the document can be pushed onto this stack
* with pushSelection(), and the most recently pushed selection can be
* popped off the stack (and thus re-selected) with popSelection().
*
* @type Range[]
*/
var selectionStack = [];
/**
* Pushes the current selection range to the selection stack such that it
* can later be restored with popSelection().
*/
var pushSelection = function pushSelection() {
// Add a range representing the current selection to the stack
var selection = $window.getSelection();
if (selection.getRangeAt && selection.rangeCount)
selectionStack.push(selection.getRangeAt(0));
};
/**
* Pops a selection range off the selection stack restoring the document's
* previous selection state. The selection range will be the most recent
* selection range pushed by pushSelection(). If there are no selection
* ranges currently on the stack, this function has no effect.
*/
var popSelection = function popSelection() {
// Pull one selection range from the stack
var range = selectionStack.pop();
if (!range)
return;
// Replace any current selection with the retrieved selection
var selection = $window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
};
/**
* Selects all nodes within the given element. This will replace the
* current selection with a new selection range that covers the element's
* contents. If the original selection should be preserved, use
* pushSelection() and popSelection().
*
* @param {Element} element
* The element whose contents should be selected.
*/
var selectAll = function selectAll(element) {
// Use the select() function defined for input elements, if available
if (element.select)
element.select();
// Fallback to manual manipulation of the selection
else {
// Generate a range which selects all nodes within the given element
var range = document.createRange();
range.selectNodeContents(element);
// Replace any current selection with the generated range
var selection = $window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
};
/**
* Sets the local clipboard, if possible, to the given text.
*
* @param {ClipboardData} data
* The data to assign to the local clipboard should be set.
*
* @return {Promise}
* A promise that will resolve if setting the clipboard was successful,
* and will reject if it failed.
*/
service.setLocalClipboard = function setLocalClipboard(data) {
var deferred = $q.defer();
try {
// Attempt to read the clipboard using the Asynchronous Clipboard
// API, if it's available
if (navigator.clipboard && navigator.clipboard.writeText) {
if (data.type === 'text/plain') {
navigator.clipboard.writeText(data.data).then(deferred.resolve, deferred.reject);
return deferred.promise;
}
}
}
// Ignore any hard failures to use Asynchronous Clipboard API, falling
// back to traditional document.execCommand()
catch (ignore) {}
// Track the originally-focused element prior to changing focus
var originalElement = document.activeElement;
pushSelection();
// Copy the given value into the clipboard DOM element
if (typeof data.data === 'string')
clipboardContent.value = data.data;
else {
clipboardContent.innerHTML = '';
var img = document.createElement('img');
img.src = URL.createObjectURL(data.data);
clipboardContent.appendChild(img);
}
// Select all data within the clipboard target
clipboardContent.focus();
selectAll(clipboardContent);
// Attempt to copy data from clipboard element into local clipboard
if (document.execCommand('copy'))
deferred.resolve();
else
deferred.reject();
// Unfocus the clipboard DOM event to avoid mobile keyboard opening,
// restoring whichever element was originally focused
clipboardContent.blur();
originalElement.focus();
popSelection();
return deferred.promise;
};
/**
* Parses the given data URL, returning its decoded contents as a new Blob.
* If the URL is not a valid data URL, null will be returned instead.
*
* @param {String} url
* The data URL to parse.
*
* @returns {Blob}
* A new Blob containing the decoded contents of the data URL, or null
* if the URL is not a valid data URL.
*/
service.parseDataURL = function parseDataURL(url) {
// Parse given string as a data URL
var result = /^data:([^;]*);base64,([a-zA-Z0-9+/]*[=]*)$/.exec(url);
if (!result)
return null;
// Pull the mimetype and base64 contents of the data URL
var type = result[1];
var data = $window.atob(result[2]);
// Convert the decoded binary string into a typed array
var buffer = new Uint8Array(data.length);
for (var i = 0; i < data.length; i++)
buffer[i] = data.charCodeAt(i);
// Produce a proper blob containing the data and type provided in
// the data URL
return new Blob([buffer], { type : type });
};
/**
* Returns the content of the given element as plain, unformatted text,
* preserving only individual characters and newlines. Formatting, images,
* etc. are not taken into account.
*
* @param {Element} element
* The element whose text content should be returned.
*
* @returns {String}
* The plain text contents of the given element, including newlines and
* spacing but otherwise without any formatting.
*/
service.getTextContent = function getTextContent(element) {
var blocks = [];
var currentBlock = '';
// For each child of the given element
var current = element.firstChild;
while (current) {
// Simply append the content of any text nodes
if (current.nodeType === Node.TEXT_NODE)
currentBlock += current.nodeValue;
// Render <br> as a newline character
else if (current.nodeName === 'BR')
currentBlock += '\n';
// Render <img> as alt text, if available
else if (current.nodeName === 'IMG')
currentBlock += current.getAttribute('alt') || '';
// For all other nodes, handling depends on whether they are
// block-level elements
else {
// If we are entering a new block context, start a new block if
// the current block is non-empty
if (currentBlock.length && $window.getComputedStyle(current).display === 'block') {
// Trim trailing newline (would otherwise inflate the line count by 1)
if (currentBlock.substring(currentBlock.length - 1) === '\n')
currentBlock = currentBlock.substring(0, currentBlock.length - 1);
// Finish current block and start a new block
blocks.push(currentBlock);
currentBlock = '';
}
// Append the content of the current element to the current block
currentBlock += service.getTextContent(current);
}
current = current.nextSibling;
}
// Add any in-progress block
if (currentBlock.length)
blocks.push(currentBlock);
// Combine all non-empty blocks, separated by newlines
return blocks.join('\n');
};
/**
* Replaces the current text content of the given element with the given
* text. To avoid affecting the position of the cursor within an editable
* element, or firing unnecessary DOM modification events, the underlying
* <code>textContent</code> property of the element is only touched if
* doing so would actually change the text.
*
* @param {Element} element
* The element whose text content should be changed.
*
* @param {String} text
* The text content to assign to the given element.
*/
service.setTextContent = function setTextContent(element, text) {
// Strip out any images
$(element).find('img').remove();
// Reset text content only if doing so will actually change the content
if (service.getTextContent(element) !== text)
element.textContent = text;
};
/**
* Returns the URL of the single image within the given element, if the
* element truly contains only one child and that child is an image. If the
* content of the element is mixed or not an image, null is returned.
*
* @param {Element} element
* The element whose image content should be retrieved.
*
* @returns {String}
* The URL of the image contained within the given element, if that
* element contains only a single child element which happens to be an
* image, or null if the content of the element is not purely an image.
*/
service.getImageContent = function getImageContent(element) {
// Return the source of the single child element, if it is an image
var firstChild = element.firstChild;
if (firstChild && firstChild.nodeName === 'IMG' && !firstChild.nextSibling)
return firstChild.getAttribute('src');
// Otherwise, the content of this element is not simply an image
return null;
};
/**
* Replaces the current contents of the given element with a single image
* having the given URL. To avoid affecting the position of the cursor
* within an editable element, or firing unnecessary DOM modification
* events, the content of the element is only touched if doing so would
* actually change content.
*
* @param {Element} element
* The element whose image content should be changed.
*
* @param {String} url
* The URL of the image which should be assigned as the contents of the
* given element.
*/
service.setImageContent = function setImageContent(element, url) {
// Retrieve the URL of the current image contents, if any
var currentImage = service.getImageContent(element);
// If the current contents are not the given image (or not an image
// at all), reassign the contents
if (currentImage !== url) {
// Clear current contents
element.innerHTML = '';
// Add a new image as the sole contents of the element
var img = document.createElement('img');
img.src = url;
element.appendChild(img);
}
};
/**
* Get the current value of the local clipboard.
*
* @return {Promise.<ClipboardData>}
* A promise that will resolve with the contents of the local clipboard
* if getting the clipboard was successful, and will reject if it
* failed.
*/
service.getLocalClipboard = function getLocalClipboard() {
// If the clipboard is already being read, do not overlap the read
// attempts; instead share the result across all requests
if (pendingRead)
return pendingRead;
var deferred = $q.defer();
try {
// Attempt to read the clipboard using the Asynchronous Clipboard
// API, if it's available
if (navigator.clipboard && navigator.clipboard.readText) {
navigator.clipboard.readText().then(function textRead(text) {
deferred.resolve(new ClipboardData({
type : 'text/plain',
data : text
}));
}, deferred.reject);
return deferred.promise;
}
}
// Ignore any hard failures to use Asynchronous Clipboard API, falling
// back to traditional document.execCommand()
catch (ignore) {}
// Track the originally-focused element prior to changing focus
var originalElement = document.activeElement;
/**
* Attempts to paste the clipboard contents into the
* currently-focused element. The promise related to the current
* attempt to read the clipboard will be resolved or rejected
* depending on whether the attempt to paste succeeds.
*/
var performPaste = function performPaste() {
// Attempt paste local clipboard into clipboard DOM element
if (document.execCommand('paste')) {
// If the pasted data is a single image, resolve with a blob
// containing that image
var currentImage = service.getImageContent(clipboardContent);
if (currentImage) {
// Convert the image's data URL into a blob
var blob = service.parseDataURL(currentImage);
if (blob) {
deferred.resolve(new ClipboardData({
type : blob.type,
data : blob
}));
}
// Reject if conversion fails
else
deferred.reject();
} // end if clipboard is an image
// Otherwise, assume the clipboard contains plain text
else
deferred.resolve(new ClipboardData({
type : 'text/plain',
data : clipboardContent.value
}));
}
// Otherwise, reading from the clipboard has failed
else
deferred.reject();
};
// Mark read attempt as in progress, cleaning up event listener and
// selection once the paste attempt has completed
pendingRead = deferred.promise['finally'](function cleanupReadAttempt() {
// Do not use future changes in focus
clipboardContent.removeEventListener('focus', performPaste);
// Unfocus the clipboard DOM event to avoid mobile keyboard opening,
// restoring whichever element was originally focused
clipboardContent.blur();
originalElement.focus();
popSelection();
// No read is pending any longer
pendingRead = null;
});
// Wait for the next event queue run before attempting to read
// clipboard data (in case the copy/cut has not yet completed)
$window.setTimeout(function deferredClipboardRead() {
pushSelection();
// Ensure clipboard element is blurred (and that the "focus" event
// will fire)
clipboardContent.blur();
clipboardContent.addEventListener('focus', performPaste);
// Clear and select the clipboard DOM element
clipboardContent.value = '';
clipboardContent.focus();
selectAll(clipboardContent);
// If focus failed to be set, we cannot read the clipboard
if (document.activeElement !== clipboardContent)
deferred.reject();
}, CLIPBOARD_READ_DELAY);
return pendingRead;
};
return service;
}]);

View File

@@ -0,0 +1,61 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.clipboard, .clipboard-service-target {
background: white;
}
.clipboard {
position: relative;
border: 1px solid #AAA;
-moz-border-radius: 0.25em;
-webkit-border-radius: 0.25em;
-khtml-border-radius: 0.25em;
border-radius: 0.25em;
width: 100%;
height: 2in;
white-space: pre;
font-size: 1em;
overflow: auto;
padding: 0.25em;
}
.clipboard p,
.clipboard div {
margin: 0;
}
.clipboard img {
max-width: 100%;
max-height: 100%;
display: block;
margin: 0 auto;
border: 1px solid black;
background: url('images/checker.png');
}
.clipboard-service-target {
position: fixed;
left: -1em;
right: -1em;
width: 1em;
height: 1em;
white-space: pre;
overflow: hidden;
}

View File

@@ -0,0 +1 @@
<textarea class="clipboard"></textarea>

View File

@@ -0,0 +1,59 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Provides the ClipboardData class used for interchange between the
* guacClipboard directive, clipboardService service, etc.
*/
angular.module('clipboard').factory('ClipboardData', [function defineClipboardData() {
/**
* Arbitrary data which can be contained by the clipboard.
*
* @constructor
* @param {ClipboardData|Object} [template={}]
* The object whose properties should be copied within the new
* ClipboardData.
*/
var ClipboardData = function ClipboardData(template) {
// Use empty object by default
template = template || {};
/**
* The mimetype of the data currently stored within the clipboard.
*
* @type String
*/
this.type = template.type || 'text/plain';
/**
* The data currently stored within the clipboard. Depending on the
* nature of the stored data, this may be either a String, a Blob, or a
* File.
*
* @type String|Blob|File
*/
this.data = template.data || '';
};
return ClipboardData;
}]);

View File

@@ -0,0 +1,63 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive which allows elements to be manually focused / blurred.
*/
angular.module('element').directive('guacFocus', ['$injector', function guacFocus($injector) {
// Required services
var $parse = $injector.get('$parse');
var $timeout = $injector.get('$timeout');
return {
restrict: 'A',
link: function linkGuacFocus($scope, $element, $attrs) {
/**
* Whether the element associated with this directive should be
* focussed.
*
* @type Boolean
*/
var guacFocus = $parse($attrs.guacFocus);
/**
* The element which will be focused / blurred.
*
* @type Element
*/
var element = $element[0];
// Set/unset focus depending on value of guacFocus
$scope.$watch(guacFocus, function updateFocus(value) {
$timeout(function updateFocusAfterRender() {
if (value)
element.focus();
else
element.blur();
});
});
} // end guacFocus link function
};
}]);

View File

@@ -0,0 +1,59 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive which stores a marker which refers to a specific element,
* allowing that element to be scrolled into view when desired.
*/
angular.module('element').directive('guacMarker', ['$injector', function guacMarker($injector) {
// Required types
var Marker = $injector.get('Marker');
// Required services
var $parse = $injector.get('$parse');
return {
restrict: 'A',
link: function linkGuacMarker($scope, $element, $attrs) {
/**
* The property in which a new Marker should be stored. The new
* Marker will refer to the element associated with this directive.
*
* @type Marker
*/
var guacMarker = $parse($attrs.guacMarker);
/**
* The element to associate with the new Marker.
*
* @type Element
*/
var element = $element[0];
// Assign new marker
guacMarker.assign($scope, new Marker(element));
}
};
}]);

View File

@@ -0,0 +1,114 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive which calls a given callback when its associated element is
* resized. This will modify the internal DOM tree of the associated element,
* and the associated element MUST have position (for example,
* "position: relative").
*/
angular.module('element').directive('guacResize', ['$document', function guacResize($document) {
return {
restrict: 'A',
link: function linkGuacResize($scope, $element, $attrs) {
/**
* The function to call whenever the associated element is
* resized. The function will be passed the width and height of
* the element, in pixels.
*
* @type Function
*/
var guacResize = $scope.$eval($attrs.guacResize);
/**
* The element which will monitored for size changes.
*
* @type Element
*/
var element = $element[0];
/**
* The resize sensor - an HTML object element.
*
* @type HTMLObjectElement
*/
var resizeSensor = $document[0].createElement('object');
/**
* The width of the associated element, in pixels.
*
* @type Number
*/
var lastWidth = element.offsetWidth;
/**
* The height of the associated element, in pixels.
*
* @type Number
*/
var lastHeight = element.offsetHeight;
/**
* Checks whether the size of the associated element has changed
* and, if so, calls the resize callback with the new width and
* height as parameters.
*/
var checkSize = function checkSize() {
// Call callback only if size actually changed
if (element.offsetWidth !== lastWidth
|| element.offsetHeight !== lastHeight) {
// Call resize callback, if defined
if (guacResize) {
$scope.$evalAsync(function elementSizeChanged() {
guacResize(element.offsetWidth, element.offsetHeight);
});
}
// Update stored size
lastWidth = element.offsetWidth;
lastHeight = element.offsetHeight;
}
};
// Register event listener once window object exists
resizeSensor.onload = function resizeSensorReady() {
resizeSensor.contentDocument.defaultView.addEventListener('resize', checkSize);
checkSize();
};
// Load blank contents
resizeSensor.className = 'resize-sensor';
resizeSensor.type = 'text/html';
resizeSensor.data = 'app/element/templates/blank.html';
// Add resize sensor to associated element
element.insertBefore(resizeSensor, element.firstChild);
} // end guacResize link function
};
}]);

View File

@@ -0,0 +1,82 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive which allows elements to be manually scrolled, and for their
* scroll state to be observed.
*/
angular.module('element').directive('guacScroll', [function guacScroll() {
return {
restrict: 'A',
link: function linkGuacScroll($scope, $element, $attrs) {
/**
* The current scroll state of the element.
*
* @type ScrollState
*/
var guacScroll = $scope.$eval($attrs.guacScroll);
/**
* The element which is being scrolled, or monitored for changes
* in scroll.
*
* @type Element
*/
var element = $element[0];
/**
* Returns the current left edge of the scrolling rectangle.
*
* @returns {Number}
* The current left edge of the scrolling rectangle.
*/
var getScrollLeft = function getScrollLeft() {
return guacScroll.left;
};
/**
* Returns the current top edge of the scrolling rectangle.
*
* @returns {Number}
* The current top edge of the scrolling rectangle.
*/
var getScrollTop = function getScrollTop() {
return guacScroll.top;
};
// Update underlying scrollLeft property when left changes
$scope.$watch(getScrollLeft, function scrollLeftChanged(left) {
element.scrollLeft = left;
guacScroll.left = element.scrollLeft;
});
// Update underlying scrollTop property when top changes
$scope.$watch(getScrollTop, function scrollTopChanged(top) {
element.scrollTop = top;
guacScroll.top = element.scrollTop;
});
} // end guacScroll link function
};
}]);

View File

@@ -0,0 +1,92 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive which allows multiple files to be uploaded. Clicking on the
* associated element will result in a file selector dialog, which then calls
* the provided callback function with any chosen files.
*/
angular.module('element').directive('guacUpload', ['$document', function guacUpload($document) {
return {
restrict: 'A',
link: function linkGuacUpload($scope, $element, $attrs) {
/**
* The function to call whenever files are chosen. The callback is
* provided a single parameter: the FileList containing all chosen
* files.
*
* @type Function
*/
var guacUpload = $scope.$eval($attrs.guacUpload);
/**
* The element which will register the drag gesture.
*
* @type Element
*/
var element = $element[0];
/**
* Internal form, containing a single file input element.
*
* @type HTMLFormElement
*/
var form = $document[0].createElement('form');
/**
* Internal file input element.
*
* @type HTMLInputElement
*/
var input = $document[0].createElement('input');
// Init input element
input.type = 'file';
input.multiple = true;
// Add input element to internal form
form.appendChild(input);
// Notify of any chosen files
input.addEventListener('change', function filesSelected() {
$scope.$apply(function setSelectedFiles() {
// Only set chosen files selection is not canceled
if (guacUpload && input.files.length > 0)
guacUpload(input.files);
// Reset selection
form.reset();
});
});
// Open file chooser when element is clicked
element.addEventListener('click', function elementClicked() {
input.click();
});
} // end guacUpload link function
};
}]);

View File

@@ -0,0 +1,24 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Module for manipulating element state, such as focus or scroll position, as
* well as handling browser events.
*/
angular.module('element', []);

View File

@@ -0,0 +1,30 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.resize-sensor {
height: 100%;
width: 100%;
position: absolute;
left: 0;
top: 0;
overflow: hidden;
border: none;
opacity: 0;
z-index: -1;
}

View File

@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>_</title>
</head>
<body></body>
</html>

View File

@@ -0,0 +1,47 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Provides the Marker class definition.
*/
angular.module('element').factory('Marker', [function defineMarker() {
/**
* Creates a new Marker which allows its associated element to be scolled
* into view as desired.
*
* @constructor
* @param {Element} element
* The element to associate with this marker.
*/
var Marker = function Marker(element) {
/**
* Scrolls scrollable elements, or the window, as needed to bring the
* element associated with this marker into view.
*/
this.scrollIntoView = function scrollIntoView() {
element.scrollIntoView();
};
};
return Marker;
}]);

View File

@@ -0,0 +1,60 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Provides the ScrollState class definition.
*/
angular.module('element').factory('ScrollState', [function defineScrollState() {
/**
* Creates a new ScrollState, representing the current scroll position of
* an arbitrary element. This constructor initializes the properties of the
* new ScrollState with the corresponding properties of the given template.
*
* @constructor
* @param {ScrollState|Object} [template={}]
* The object whose properties should be copied within the new
* ScrollState.
*/
var ScrollState = function ScrollState(template) {
// Use empty object by default
template = template || {};
/**
* The left edge of the view rectangle within the scrollable area. This
* value naturally increases as the user scrolls right.
*
* @type Number
*/
this.left = template.left || 0;
/**
* The top edge of the view rectangle within the scrollable area. This
* value naturally increases as the user scrolls down.
*
* @type Number
*/
this.top = template.top || 0;
};
return ScrollState;
}]);

View File

@@ -0,0 +1,37 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Controller for checkbox fields.
*/
angular.module('form').controller('checkboxFieldController', ['$scope',
function checkboxFieldController($scope) {
// Update typed value when model is changed
$scope.$watch('model', function modelChanged(model) {
$scope.typedValue = (model === $scope.field.options[0]);
});
// Update string value in model when typed value is changed
$scope.$watch('typedValue', function typedValueChanged(typedValue) {
$scope.model = (typedValue ? $scope.field.options[0] : '');
});
}]);

View File

@@ -0,0 +1,89 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Controller for date fields.
*/
angular.module('form').controller('dateFieldController', ['$scope', '$injector',
function dateFieldController($scope, $injector) {
// Required services
var $filter = $injector.get('$filter');
/**
* Options which dictate the behavior of the input field model, as defined
* by https://docs.angularjs.org/api/ng/directive/ngModelOptions
*
* @type Object.<String, String>
*/
$scope.modelOptions = {
/**
* Space-delimited list of events on which the model will be updated.
*
* @type String
*/
updateOn : 'blur',
/**
* The time zone to use when reading/writing the Date object of the
* model.
*
* @type String
*/
timezone : 'UTC'
};
/**
* Parses the date components of the given string into a Date with only the
* date components set. The resulting Date will be in the UTC timezone,
* with the time left as midnight. The input string must be in the format
* YYYY-MM-DD (zero-padded).
*
* @param {String} str
* The date string to parse.
*
* @returns {Date}
* A Date object, in the UTC timezone, with only the date components
* set.
*/
var parseDate = function parseDate(str) {
// Parse date, return blank if invalid
var parsedDate = new Date(str + 'T00:00Z');
if (isNaN(parsedDate.getTime()))
return null;
return parsedDate;
};
// Update typed value when model is changed
$scope.$watch('model', function modelChanged(model) {
$scope.typedValue = (model ? parseDate(model) : null);
});
// Update string value in model when typed value is changed
$scope.$watch('typedValue', function typedValueChanged(typedValue) {
$scope.model = (typedValue ? $filter('date')(typedValue, 'yyyy-MM-dd', 'UTC') : '');
});
}]);

View File

@@ -0,0 +1,52 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Controller for the language field type. The language field type allows the
* user to select a language from the set of languages supported by the
* Guacamole web application.
*/
angular.module('form').controller('languageFieldController', ['$scope', '$injector',
function languageFieldController($scope, $injector) {
// Required services
var languageService = $injector.get('languageService');
var requestService = $injector.get('requestService');
/**
* A map of all available language keys to their human-readable
* names.
*
* @type Object.<String, String>
*/
$scope.languages = null;
// Retrieve defined languages
languageService.getLanguages().then(function languagesRetrieved(languages) {
$scope.languages = languages;
}, requestService.DIE);
// Interpret undefined/null as empty string
$scope.$watch('model', function setModel(model) {
if (!model && model !== '')
$scope.model = '';
});
}]);

View File

@@ -0,0 +1,37 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Controller for number fields.
*/
angular.module('form').controller('numberFieldController', ['$scope',
function numberFieldController($scope) {
// Update typed value when model is changed
$scope.$watch('model', function modelChanged(model) {
$scope.typedValue = (model ? Number(model) : null);
});
// Update string value in model when typed value is changed
$scope.$watch('typedValue', function typedValueChanged(typedValue) {
$scope.model = ((typedValue || typedValue === 0) ? typedValue.toString() : '');
});
}]);

View File

@@ -0,0 +1,72 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Controller for password fields.
*/
angular.module('form').controller('passwordFieldController', ['$scope',
function passwordFieldController($scope) {
/**
* The type to use for the input field. By default, the input field will
* have the type 'password', and thus will be masked.
*
* @type String
* @default 'password'
*/
$scope.passwordInputType = 'password';
/**
* Returns a string which describes the action the next call to
* togglePassword() will have.
*
* @return {String}
* A string which describes the action the next call to
* togglePassword() will have.
*/
$scope.getTogglePasswordHelpText = function getTogglePasswordHelpText() {
// If password is hidden, togglePassword() will show the password
if ($scope.passwordInputType === 'password')
return 'FORM.HELP_SHOW_PASSWORD';
// If password is shown, togglePassword() will hide the password
return 'FORM.HELP_HIDE_PASSWORD';
};
/**
* Toggles visibility of the field contents, if this field is a
* password field. Initially, password contents are masked
* (invisible).
*/
$scope.togglePassword = function togglePassword() {
// If password is hidden, show the password
if ($scope.passwordInputType === 'password')
$scope.passwordInputType = 'text';
// If password is shown, hide the password
else
$scope.passwordInputType = 'password';
};
}]);

View File

@@ -0,0 +1,32 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Controller for the redirect field, which redirects the user to the provided
* URL.
*/
angular.module('form').controller('redirectFieldController', ['$scope','$window',
function redirectFieldController($scope,$window) {
/**
* Redirect the user to the provided URL.
*/
$window.location.href = $scope.field.redirectUrl;
}]);

View File

@@ -0,0 +1,33 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Controller for select fields.
*/
angular.module('form').controller('selectFieldController', ['$scope', '$injector',
function selectFieldController($scope, $injector) {
// Interpret undefined/null as empty string
$scope.$watch('model', function setModel(model) {
if (!model && model !== '')
$scope.model = '';
});
}]);

View File

@@ -0,0 +1,142 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Controller for terminal color scheme fields.
*/
angular.module('form').controller('terminalColorSchemeFieldController', ['$scope', '$injector',
function terminalColorSchemeFieldController($scope, $injector) {
// Required types
var ColorScheme = $injector.get('ColorScheme');
/**
* The currently selected color scheme. If a pre-defined color scheme is
* selected, this will be the connection parameter value associated with
* that color scheme. If a custom color scheme is selected, this will be
* the string "custom".
*
* @type String
*/
$scope.selectedColorScheme = '';
/**
* The current custom color scheme, if a custom color scheme has been
* specified. If no custom color scheme has yet been specified, this will
* be a ColorScheme instance that has been initialized to the default
* colors.
*
* @type ColorScheme
*/
$scope.customColorScheme = new ColorScheme();
/**
* The array of colors to include within the color picker as pre-defined
* options for convenience.
*
* @type String[]
*/
$scope.defaultPalette = new ColorScheme().colors;
/**
* Whether the raw details of the custom color scheme should be shown. By
* default, such details are hidden.
*
* @type Boolean
*/
$scope.detailsShown = false;
/**
* The palette indices of all colors which are considered low-intensity.
*
* @type Number[]
*/
$scope.lowIntensity = [ 0, 1, 2, 3, 4, 5, 6, 7 ];
/**
* The palette indices of all colors which are considered high-intensity.
*
* @type Number[]
*/
$scope.highIntensity = [ 8, 9, 10, 11, 12, 13, 14, 15 ];
/**
* The string value which is assigned to selectedColorScheme if a custom
* color scheme is selected.
*
* @constant
* @type String
*/
var CUSTOM_COLOR_SCHEME = 'custom';
/**
* Returns whether a custom color scheme has been selected.
*
* @returns {Boolean}
* true if a custom color scheme has been selected, false otherwise.
*/
$scope.isCustom = function isCustom() {
return $scope.selectedColorScheme === CUSTOM_COLOR_SCHEME;
};
/**
* Shows the raw details of the custom color scheme. If the details are
* already shown, this function has no effect.
*/
$scope.showDetails = function showDetails() {
$scope.detailsShown = true;
};
/**
* Hides the raw details of the custom color scheme. If the details are
* already hidden, this function has no effect.
*/
$scope.hideDetails = function hideDetails() {
$scope.detailsShown = false;
};
// Keep selected color scheme and custom color scheme in sync with changes
// to model
$scope.$watch('model', function modelChanged(model) {
if ($scope.selectedColorScheme === CUSTOM_COLOR_SCHEME || (model && !_.includes($scope.field.options, model))) {
$scope.customColorScheme = ColorScheme.fromString(model);
$scope.selectedColorScheme = CUSTOM_COLOR_SCHEME;
}
else
$scope.selectedColorScheme = model || '';
});
// Keep model in sync with changes to selected color scheme
$scope.$watch('selectedColorScheme', function selectedColorSchemeChanged(selectedColorScheme) {
if (!selectedColorScheme)
$scope.model = '';
else if (selectedColorScheme === CUSTOM_COLOR_SCHEME)
$scope.model = ColorScheme.toString($scope.customColorScheme);
else
$scope.model = selectedColorScheme;
});
// Keep model in sync with changes to custom color scheme
$scope.$watch('customColorScheme', function customColorSchemeChanged(customColorScheme) {
if ($scope.selectedColorScheme === CUSTOM_COLOR_SCHEME)
$scope.model = ColorScheme.toString(customColorScheme);
}, true);
}]);

View File

@@ -0,0 +1,40 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Controller for text fields.
*/
angular.module('form').controller('textFieldController', ['$scope', '$injector',
function textFieldController($scope, $injector) {
/**
* The ID of the datalist element that should be associated with the text
* field, providing a set of known-good values. If no such values are
* defined, this will be null.
*
* @type String
*/
$scope.dataListId = null;
// Generate unique ID for datalist, if applicable
if ($scope.field.options && $scope.field.options.length)
$scope.dataListId = $scope.fieldId + '-datalist';
}]);

View File

@@ -0,0 +1,89 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Controller for time fields.
*/
angular.module('form').controller('timeFieldController', ['$scope', '$injector',
function timeFieldController($scope, $injector) {
// Required services
var $filter = $injector.get('$filter');
/**
* Options which dictate the behavior of the input field model, as defined
* by https://docs.angularjs.org/api/ng/directive/ngModelOptions
*
* @type Object.<String, String>
*/
$scope.modelOptions = {
/**
* Space-delimited list of events on which the model will be updated.
*
* @type String
*/
updateOn : 'blur',
/**
* The time zone to use when reading/writing the Date object of the
* model.
*
* @type String
*/
timezone : 'UTC'
};
/**
* Parses the time components of the given string into a Date with only the
* time components set. The resulting Date will be in the UTC timezone,
* with the date left as 1970-01-01. The input string must be in the format
* HH:MM:SS (zero-padded, 24-hour).
*
* @param {String} str
* The time string to parse.
*
* @returns {Date}
* A Date object, in the UTC timezone, with only the time components
* set.
*/
var parseTime = function parseTime(str) {
// Parse time, return blank if invalid
var parsedDate = new Date('1970-01-01T' + str + 'Z');
if (isNaN(parsedDate.getTime()))
return null;
return parsedDate;
};
// Update typed value when model is changed
$scope.$watch('model', function modelChanged(model) {
$scope.typedValue = (model ? parseTime(model) : null);
});
// Update string value in model when typed value is changed
$scope.$watch('typedValue', function typedValueChanged(typedValue) {
$scope.model = (typedValue ? $filter('date')(typedValue, 'HH:mm:ss', 'UTC') : '');
});
}]);

View File

@@ -0,0 +1,708 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Controller for time zone fields. Time zone fields use IANA time zone
* database identifiers as the standard representation for each supported time
* zone. These identifiers are also legal Java time zone IDs.
*/
angular.module('form').controller('timeZoneFieldController', ['$scope', '$injector',
function timeZoneFieldController($scope, $injector) {
/**
* Map of time zone regions to the map of all time zone name/ID pairs
* within those regions.
*
* @type Object.<String, Object.<String, String>>
*/
$scope.timeZones = {
"Africa" : {
"Abidjan" : "Africa/Abidjan",
"Accra" : "Africa/Accra",
"Addis Ababa" : "Africa/Addis_Ababa",
"Algiers" : "Africa/Algiers",
"Asmara" : "Africa/Asmara",
"Asmera" : "Africa/Asmera",
"Bamako" : "Africa/Bamako",
"Bangui" : "Africa/Bangui",
"Banjul" : "Africa/Banjul",
"Bissau" : "Africa/Bissau",
"Blantyre" : "Africa/Blantyre",
"Brazzaville" : "Africa/Brazzaville",
"Bujumbura" : "Africa/Bujumbura",
"Cairo" : "Africa/Cairo",
"Casablanca" : "Africa/Casablanca",
"Ceuta" : "Africa/Ceuta",
"Conakry" : "Africa/Conakry",
"Dakar" : "Africa/Dakar",
"Dar es Salaam" : "Africa/Dar_es_Salaam",
"Djibouti" : "Africa/Djibouti",
"Douala" : "Africa/Douala",
"El Aaiun" : "Africa/El_Aaiun",
"Freetown" : "Africa/Freetown",
"Gaborone" : "Africa/Gaborone",
"Harare" : "Africa/Harare",
"Johannesburg" : "Africa/Johannesburg",
"Juba" : "Africa/Juba",
"Kampala" : "Africa/Kampala",
"Khartoum" : "Africa/Khartoum",
"Kigali" : "Africa/Kigali",
"Kinshasa" : "Africa/Kinshasa",
"Lagos" : "Africa/Lagos",
"Libreville" : "Africa/Libreville",
"Lome" : "Africa/Lome",
"Luanda" : "Africa/Luanda",
"Lubumbashi" : "Africa/Lubumbashi",
"Lusaka" : "Africa/Lusaka",
"Malabo" : "Africa/Malabo",
"Maputo" : "Africa/Maputo",
"Maseru" : "Africa/Maseru",
"Mbabane" : "Africa/Mbabane",
"Mogadishu" : "Africa/Mogadishu",
"Monrovia" : "Africa/Monrovia",
"Nairobi" : "Africa/Nairobi",
"Ndjamena" : "Africa/Ndjamena",
"Niamey" : "Africa/Niamey",
"Nouakchott" : "Africa/Nouakchott",
"Ouagadougou" : "Africa/Ouagadougou",
"Porto-Novo" : "Africa/Porto-Novo",
"Sao Tome" : "Africa/Sao_Tome",
"Timbuktu" : "Africa/Timbuktu",
"Tripoli" : "Africa/Tripoli",
"Tunis" : "Africa/Tunis",
"Windhoek" : "Africa/Windhoek"
},
"America" : {
"Adak" : "America/Adak",
"Anchorage" : "America/Anchorage",
"Anguilla" : "America/Anguilla",
"Antigua" : "America/Antigua",
"Araguaina" : "America/Araguaina",
"Argentina / Buenos Aires" : "America/Argentina/Buenos_Aires",
"Argentina / Catamarca" : "America/Argentina/Catamarca",
"Argentina / Comodoro Rivadavia" : "America/Argentina/ComodRivadavia",
"Argentina / Cordoba" : "America/Argentina/Cordoba",
"Argentina / Jujuy" : "America/Argentina/Jujuy",
"Argentina / La Rioja" : "America/Argentina/La_Rioja",
"Argentina / Mendoza" : "America/Argentina/Mendoza",
"Argentina / Rio Gallegos" : "America/Argentina/Rio_Gallegos",
"Argentina / Salta" : "America/Argentina/Salta",
"Argentina / San Juan" : "America/Argentina/San_Juan",
"Argentina / San Luis" : "America/Argentina/San_Luis",
"Argentina / Tucuman" : "America/Argentina/Tucuman",
"Argentina / Ushuaia" : "America/Argentina/Ushuaia",
"Aruba" : "America/Aruba",
"Asuncion" : "America/Asuncion",
"Atikokan" : "America/Atikokan",
"Atka" : "America/Atka",
"Bahia" : "America/Bahia",
"Bahia Banderas" : "America/Bahia_Banderas",
"Barbados" : "America/Barbados",
"Belem" : "America/Belem",
"Belize" : "America/Belize",
"Blanc-Sablon" : "America/Blanc-Sablon",
"Boa Vista" : "America/Boa_Vista",
"Bogota" : "America/Bogota",
"Boise" : "America/Boise",
"Buenos Aires" : "America/Buenos_Aires",
"Cambridge Bay" : "America/Cambridge_Bay",
"Campo Grande" : "America/Campo_Grande",
"Cancun" : "America/Cancun",
"Caracas" : "America/Caracas",
"Catamarca" : "America/Catamarca",
"Cayenne" : "America/Cayenne",
"Cayman" : "America/Cayman",
"Chicago" : "America/Chicago",
"Chihuahua" : "America/Chihuahua",
"Coral Harbour" : "America/Coral_Harbour",
"Cordoba" : "America/Cordoba",
"Costa Rica" : "America/Costa_Rica",
"Creston" : "America/Creston",
"Cuiaba" : "America/Cuiaba",
"Curacao" : "America/Curacao",
"Danmarkshavn" : "America/Danmarkshavn",
"Dawson" : "America/Dawson",
"Dawson Creek" : "America/Dawson_Creek",
"Denver" : "America/Denver",
"Detroit" : "America/Detroit",
"Dominica" : "America/Dominica",
"Edmonton" : "America/Edmonton",
"Eirunepe" : "America/Eirunepe",
"El Salvador" : "America/El_Salvador",
"Ensenada" : "America/Ensenada",
"Fort Wayne" : "America/Fort_Wayne",
"Fortaleza" : "America/Fortaleza",
"Glace Bay" : "America/Glace_Bay",
"Godthab" : "America/Godthab",
"Goose Bay" : "America/Goose_Bay",
"Grand Turk" : "America/Grand_Turk",
"Grenada" : "America/Grenada",
"Guadeloupe" : "America/Guadeloupe",
"Guatemala" : "America/Guatemala",
"Guayaquil" : "America/Guayaquil",
"Guyana" : "America/Guyana",
"Halifax" : "America/Halifax",
"Havana" : "America/Havana",
"Hermosillo" : "America/Hermosillo",
"Indiana / Indianapolis" : "America/Indiana/Indianapolis",
"Indiana / Knox" : "America/Indiana/Knox",
"Indiana / Marengo" : "America/Indiana/Marengo",
"Indiana / Petersburg" : "America/Indiana/Petersburg",
"Indiana / Tell City" : "America/Indiana/Tell_City",
"Indiana / Vevay" : "America/Indiana/Vevay",
"Indiana / Vincennes" : "America/Indiana/Vincennes",
"Indiana / Winamac" : "America/Indiana/Winamac",
"Indianapolis" : "America/Indianapolis",
"Inuvik" : "America/Inuvik",
"Iqaluit" : "America/Iqaluit",
"Jamaica" : "America/Jamaica",
"Jujuy" : "America/Jujuy",
"Juneau" : "America/Juneau",
"Kentucky / Louisville" : "America/Kentucky/Louisville",
"Kentucky / Monticello" : "America/Kentucky/Monticello",
"Kralendijk" : "America/Kralendijk",
"La Paz" : "America/La_Paz",
"Lima" : "America/Lima",
"Los Angeles" : "America/Los_Angeles",
"Louisville" : "America/Louisville",
"Lower Princes" : "America/Lower_Princes",
"Maceio" : "America/Maceio",
"Managua" : "America/Managua",
"Manaus" : "America/Manaus",
"Marigot" : "America/Marigot",
"Martinique" : "America/Martinique",
"Matamoros" : "America/Matamoros",
"Mazatlan" : "America/Mazatlan",
"Mendoza" : "America/Mendoza",
"Menominee" : "America/Menominee",
"Merida" : "America/Merida",
"Metlakatla" : "America/Metlakatla",
"Mexico City" : "America/Mexico_City",
"Miquelon" : "America/Miquelon",
"Moncton" : "America/Moncton",
"Monterrey" : "America/Monterrey",
"Montevideo" : "America/Montevideo",
"Montreal" : "America/Montreal",
"Montserrat" : "America/Montserrat",
"Nassau" : "America/Nassau",
"New York" : "America/New_York",
"Nipigon" : "America/Nipigon",
"Nome" : "America/Nome",
"Noronha" : "America/Noronha",
"North Dakota / Beulah" : "America/North_Dakota/Beulah",
"North Dakota / Center" : "America/North_Dakota/Center",
"North Dakota / New Salem" : "America/North_Dakota/New_Salem",
"Ojinaga" : "America/Ojinaga",
"Panama" : "America/Panama",
"Pangnirtung" : "America/Pangnirtung",
"Paramaribo" : "America/Paramaribo",
"Phoenix" : "America/Phoenix",
"Port-au-Prince" : "America/Port-au-Prince",
"Port of Spain" : "America/Port_of_Spain",
"Porto Acre" : "America/Porto_Acre",
"Porto Velho" : "America/Porto_Velho",
"Puerto Rico" : "America/Puerto_Rico",
"Rainy River" : "America/Rainy_River",
"Rankin Inlet" : "America/Rankin_Inlet",
"Recife" : "America/Recife",
"Regina" : "America/Regina",
"Resolute" : "America/Resolute",
"Rio Branco" : "America/Rio_Branco",
"Rosario" : "America/Rosario",
"Santa Isabel" : "America/Santa_Isabel",
"Santarem" : "America/Santarem",
"Santiago" : "America/Santiago",
"Santo Domingo" : "America/Santo_Domingo",
"Sao Paulo" : "America/Sao_Paulo",
"Scoresbysund" : "America/Scoresbysund",
"Shiprock" : "America/Shiprock",
"Sitka" : "America/Sitka",
"St. Barthelemy" : "America/St_Barthelemy",
"St. Johns" : "America/St_Johns",
"St. Kitts" : "America/St_Kitts",
"St. Lucia" : "America/St_Lucia",
"St. Thomas" : "America/St_Thomas",
"St. Vincent" : "America/St_Vincent",
"Swift Current" : "America/Swift_Current",
"Tegucigalpa" : "America/Tegucigalpa",
"Thule" : "America/Thule",
"Thunder Bay" : "America/Thunder_Bay",
"Tijuana" : "America/Tijuana",
"Toronto" : "America/Toronto",
"Tortola" : "America/Tortola",
"Vancouver" : "America/Vancouver",
"Virgin" : "America/Virgin",
"Whitehorse" : "America/Whitehorse",
"Winnipeg" : "America/Winnipeg",
"Yakutat" : "America/Yakutat",
"Yellowknife" : "America/Yellowknife"
},
"Antarctica" : {
"Casey" : "Antarctica/Casey",
"Davis" : "Antarctica/Davis",
"Dumont d'Urville" : "Antarctica/DumontDUrville",
"Macquarie" : "Antarctica/Macquarie",
"Mawson" : "Antarctica/Mawson",
"McMurdo" : "Antarctica/McMurdo",
"Palmer" : "Antarctica/Palmer",
"Rothera" : "Antarctica/Rothera",
"South Pole" : "Antarctica/South_Pole",
"Syowa" : "Antarctica/Syowa",
"Troll" : "Antarctica/Troll",
"Vostok" : "Antarctica/Vostok"
},
"Arctic" : {
"Longyearbyen" : "Arctic/Longyearbyen"
},
"Asia" : {
"Aden" : "Asia/Aden",
"Almaty" : "Asia/Almaty",
"Amman" : "Asia/Amman",
"Anadyr" : "Asia/Anadyr",
"Aqtau" : "Asia/Aqtau",
"Aqtobe" : "Asia/Aqtobe",
"Ashgabat" : "Asia/Ashgabat",
"Ashkhabad" : "Asia/Ashkhabad",
"Baghdad" : "Asia/Baghdad",
"Bahrain" : "Asia/Bahrain",
"Baku" : "Asia/Baku",
"Bangkok" : "Asia/Bangkok",
"Beirut" : "Asia/Beirut",
"Bishkek" : "Asia/Bishkek",
"Brunei" : "Asia/Brunei",
"Calcutta" : "Asia/Calcutta",
"Chita" : "Asia/Chita",
"Choibalsan" : "Asia/Choibalsan",
"Chongqing" : "Asia/Chongqing",
"Colombo" : "Asia/Colombo",
"Dacca" : "Asia/Dacca",
"Damascus" : "Asia/Damascus",
"Dhaka" : "Asia/Dhaka",
"Dili" : "Asia/Dili",
"Dubai" : "Asia/Dubai",
"Dushanbe" : "Asia/Dushanbe",
"Gaza" : "Asia/Gaza",
"Harbin" : "Asia/Harbin",
"Hebron" : "Asia/Hebron",
"Ho Chi Minh" : "Asia/Ho_Chi_Minh",
"Hong Kong" : "Asia/Hong_Kong",
"Hovd" : "Asia/Hovd",
"Irkutsk" : "Asia/Irkutsk",
"Istanbul" : "Asia/Istanbul",
"Jakarta" : "Asia/Jakarta",
"Jayapura" : "Asia/Jayapura",
"Jerusalem" : "Asia/Jerusalem",
"Kabul" : "Asia/Kabul",
"Kamchatka" : "Asia/Kamchatka",
"Karachi" : "Asia/Karachi",
"Kashgar" : "Asia/Kashgar",
"Kathmandu" : "Asia/Kathmandu",
"Katmandu" : "Asia/Katmandu",
"Khandyga" : "Asia/Khandyga",
"Kolkata" : "Asia/Kolkata",
"Krasnoyarsk" : "Asia/Krasnoyarsk",
"Kuala Lumpur" : "Asia/Kuala_Lumpur",
"Kuching" : "Asia/Kuching",
"Kuwait" : "Asia/Kuwait",
"Macao" : "Asia/Macao",
"Macau" : "Asia/Macau",
"Magadan" : "Asia/Magadan",
"Makassar" : "Asia/Makassar",
"Manila" : "Asia/Manila",
"Muscat" : "Asia/Muscat",
"Nicosia" : "Asia/Nicosia",
"Novokuznetsk" : "Asia/Novokuznetsk",
"Novosibirsk" : "Asia/Novosibirsk",
"Omsk" : "Asia/Omsk",
"Oral" : "Asia/Oral",
"Phnom Penh" : "Asia/Phnom_Penh",
"Pontianak" : "Asia/Pontianak",
"Pyongyang" : "Asia/Pyongyang",
"Qatar" : "Asia/Qatar",
"Qyzylorda" : "Asia/Qyzylorda",
"Rangoon" : "Asia/Rangoon",
"Riyadh" : "Asia/Riyadh",
"Saigon" : "Asia/Saigon",
"Sakhalin" : "Asia/Sakhalin",
"Samarkand" : "Asia/Samarkand",
"Seoul" : "Asia/Seoul",
"Shanghai" : "Asia/Shanghai",
"Singapore" : "Asia/Singapore",
"Srednekolymsk" : "Asia/Srednekolymsk",
"Taipei" : "Asia/Taipei",
"Tashkent" : "Asia/Tashkent",
"Tbilisi" : "Asia/Tbilisi",
"Tehran" : "Asia/Tehran",
"Tel Aviv" : "Asia/Tel_Aviv",
"Thimbu" : "Asia/Thimbu",
"Thimphu" : "Asia/Thimphu",
"Tokyo" : "Asia/Tokyo",
"Ujung Pandang" : "Asia/Ujung_Pandang",
"Ulaanbaatar" : "Asia/Ulaanbaatar",
"Ulan Bator" : "Asia/Ulan_Bator",
"Urumqi" : "Asia/Urumqi",
"Ust-Nera" : "Asia/Ust-Nera",
"Vientiane" : "Asia/Vientiane",
"Vladivostok" : "Asia/Vladivostok",
"Yakutsk" : "Asia/Yakutsk",
"Yekaterinburg" : "Asia/Yekaterinburg",
"Yerevan" : "Asia/Yerevan"
},
"Atlantic" : {
"Azores" : "Atlantic/Azores",
"Bermuda" : "Atlantic/Bermuda",
"Canary" : "Atlantic/Canary",
"Cape Verde" : "Atlantic/Cape_Verde",
"Faeroe" : "Atlantic/Faeroe",
"Faroe" : "Atlantic/Faroe",
"Jan Mayen" : "Atlantic/Jan_Mayen",
"Madeira" : "Atlantic/Madeira",
"Reykjavik" : "Atlantic/Reykjavik",
"South Georgia" : "Atlantic/South_Georgia",
"St. Helena" : "Atlantic/St_Helena",
"Stanley" : "Atlantic/Stanley"
},
"Australia" : {
"Adelaide" : "Australia/Adelaide",
"Brisbane" : "Australia/Brisbane",
"Broken Hill" : "Australia/Broken_Hill",
"Canberra" : "Australia/Canberra",
"Currie" : "Australia/Currie",
"Darwin" : "Australia/Darwin",
"Eucla" : "Australia/Eucla",
"Hobart" : "Australia/Hobart",
"Lindeman" : "Australia/Lindeman",
"Lord Howe" : "Australia/Lord_Howe",
"Melbourne" : "Australia/Melbourne",
"North" : "Australia/North",
"Perth" : "Australia/Perth",
"Queensland" : "Australia/Queensland",
"South" : "Australia/South",
"Sydney" : "Australia/Sydney",
"Tasmania" : "Australia/Tasmania",
"Victoria" : "Australia/Victoria",
"West" : "Australia/West",
"Yancowinna" : "Australia/Yancowinna"
},
"Brazil" : {
"Acre" : "Brazil/Acre",
"Fernando de Noronha" : "Brazil/DeNoronha",
"East" : "Brazil/East",
"West" : "Brazil/West"
},
"Canada" : {
"Atlantic" : "Canada/Atlantic",
"Central" : "Canada/Central",
"Eastern" : "Canada/Eastern",
"Mountain" : "Canada/Mountain",
"Newfoundland" : "Canada/Newfoundland",
"Pacific" : "Canada/Pacific",
"Saskatchewan" : "Canada/Saskatchewan",
"Yukon" : "Canada/Yukon"
},
"Chile" : {
"Continental" : "Chile/Continental",
"Easter Island" : "Chile/EasterIsland"
},
"Europe" : {
"Amsterdam" : "Europe/Amsterdam",
"Andorra" : "Europe/Andorra",
"Athens" : "Europe/Athens",
"Belfast" : "Europe/Belfast",
"Belgrade" : "Europe/Belgrade",
"Berlin" : "Europe/Berlin",
"Bratislava" : "Europe/Bratislava",
"Brussels" : "Europe/Brussels",
"Bucharest" : "Europe/Bucharest",
"Budapest" : "Europe/Budapest",
"Busingen" : "Europe/Busingen",
"Chisinau" : "Europe/Chisinau",
"Copenhagen" : "Europe/Copenhagen",
"Dublin" : "Europe/Dublin",
"Gibraltar" : "Europe/Gibraltar",
"Guernsey" : "Europe/Guernsey",
"Helsinki" : "Europe/Helsinki",
"Isle of Man" : "Europe/Isle_of_Man",
"Istanbul" : "Europe/Istanbul",
"Jersey" : "Europe/Jersey",
"Kaliningrad" : "Europe/Kaliningrad",
"Kiev" : "Europe/Kiev",
"Lisbon" : "Europe/Lisbon",
"Ljubljana" : "Europe/Ljubljana",
"London" : "Europe/London",
"Luxembourg" : "Europe/Luxembourg",
"Madrid" : "Europe/Madrid",
"Malta" : "Europe/Malta",
"Mariehamn" : "Europe/Mariehamn",
"Minsk" : "Europe/Minsk",
"Monaco" : "Europe/Monaco",
"Moscow" : "Europe/Moscow",
"Nicosia" : "Europe/Nicosia",
"Oslo" : "Europe/Oslo",
"Paris" : "Europe/Paris",
"Podgorica" : "Europe/Podgorica",
"Prague" : "Europe/Prague",
"Riga" : "Europe/Riga",
"Rome" : "Europe/Rome",
"Samara" : "Europe/Samara",
"San Marino" : "Europe/San_Marino",
"Sarajevo" : "Europe/Sarajevo",
"Simferopol" : "Europe/Simferopol",
"Skopje" : "Europe/Skopje",
"Sofia" : "Europe/Sofia",
"Stockholm" : "Europe/Stockholm",
"Tallinn" : "Europe/Tallinn",
"Tirane" : "Europe/Tirane",
"Tiraspol" : "Europe/Tiraspol",
"Uzhgorod" : "Europe/Uzhgorod",
"Vaduz" : "Europe/Vaduz",
"Vatican" : "Europe/Vatican",
"Vienna" : "Europe/Vienna",
"Vilnius" : "Europe/Vilnius",
"Volgograd" : "Europe/Volgograd",
"Warsaw" : "Europe/Warsaw",
"Zagreb" : "Europe/Zagreb",
"Zaporozhye" : "Europe/Zaporozhye",
"Zurich" : "Europe/Zurich"
},
"GMT" : {
"GMT-14" : "Etc/GMT-14",
"GMT-13" : "Etc/GMT-13",
"GMT-12" : "Etc/GMT-12",
"GMT-11" : "Etc/GMT-11",
"GMT-10" : "Etc/GMT-10",
"GMT-9" : "Etc/GMT-9",
"GMT-8" : "Etc/GMT-8",
"GMT-7" : "Etc/GMT-7",
"GMT-6" : "Etc/GMT-6",
"GMT-5" : "Etc/GMT-5",
"GMT-4" : "Etc/GMT-4",
"GMT-3" : "Etc/GMT-3",
"GMT-2" : "Etc/GMT-2",
"GMT-1" : "Etc/GMT-1",
"GMT+0" : "Etc/GMT+0",
"GMT+1" : "Etc/GMT+1",
"GMT+2" : "Etc/GMT+2",
"GMT+3" : "Etc/GMT+3",
"GMT+4" : "Etc/GMT+4",
"GMT+5" : "Etc/GMT+5",
"GMT+6" : "Etc/GMT+6",
"GMT+7" : "Etc/GMT+7",
"GMT+8" : "Etc/GMT+8",
"GMT+9" : "Etc/GMT+9",
"GMT+10" : "Etc/GMT+10",
"GMT+11" : "Etc/GMT+11",
"GMT+12" : "Etc/GMT+12"
},
"Indian" : {
"Antananarivo" : "Indian/Antananarivo",
"Chagos" : "Indian/Chagos",
"Christmas" : "Indian/Christmas",
"Cocos" : "Indian/Cocos",
"Comoro" : "Indian/Comoro",
"Kerguelen" : "Indian/Kerguelen",
"Mahe" : "Indian/Mahe",
"Maldives" : "Indian/Maldives",
"Mauritius" : "Indian/Mauritius",
"Mayotte" : "Indian/Mayotte",
"Reunion" : "Indian/Reunion"
},
"Mexico" : {
"Baja Norte" : "Mexico/BajaNorte",
"Baja Sur" : "Mexico/BajaSur",
"General" : "Mexico/General"
},
"Pacific" : {
"Apia" : "Pacific/Apia",
"Auckland" : "Pacific/Auckland",
"Bougainville" : "Pacific/Bougainville",
"Chatham" : "Pacific/Chatham",
"Chuuk" : "Pacific/Chuuk",
"Easter" : "Pacific/Easter",
"Efate" : "Pacific/Efate",
"Enderbury" : "Pacific/Enderbury",
"Fakaofo" : "Pacific/Fakaofo",
"Fiji" : "Pacific/Fiji",
"Funafuti" : "Pacific/Funafuti",
"Galapagos" : "Pacific/Galapagos",
"Gambier" : "Pacific/Gambier",
"Guadalcanal" : "Pacific/Guadalcanal",
"Guam" : "Pacific/Guam",
"Honolulu" : "Pacific/Honolulu",
"Johnston" : "Pacific/Johnston",
"Kiritimati" : "Pacific/Kiritimati",
"Kosrae" : "Pacific/Kosrae",
"Kwajalein" : "Pacific/Kwajalein",
"Majuro" : "Pacific/Majuro",
"Marquesas" : "Pacific/Marquesas",
"Midway" : "Pacific/Midway",
"Nauru" : "Pacific/Nauru",
"Niue" : "Pacific/Niue",
"Norfolk" : "Pacific/Norfolk",
"Noumea" : "Pacific/Noumea",
"Pago Pago" : "Pacific/Pago_Pago",
"Palau" : "Pacific/Palau",
"Pitcairn" : "Pacific/Pitcairn",
"Pohnpei" : "Pacific/Pohnpei",
"Ponape" : "Pacific/Ponape",
"Port Moresby" : "Pacific/Port_Moresby",
"Rarotonga" : "Pacific/Rarotonga",
"Saipan" : "Pacific/Saipan",
"Samoa" : "Pacific/Samoa",
"Tahiti" : "Pacific/Tahiti",
"Tarawa" : "Pacific/Tarawa",
"Tongatapu" : "Pacific/Tongatapu",
"Truk" : "Pacific/Truk",
"Wake" : "Pacific/Wake",
"Wallis" : "Pacific/Wallis",
"Yap" : "Pacific/Yap"
}
};
/**
* All selectable regions.
*
* @type String[]
*/
$scope.regions = (function collectRegions() {
// Start with blank entry
var regions = [ '' ];
// Add each available region
for (var region in $scope.timeZones)
regions.push(region);
return regions;
})();
/**
* Direct mapping of all time zone IDs to the region containing that ID.
*
* @type Object.<String, String>
*/
var timeZoneRegions = (function mapRegions() {
var regions = {};
// For each available region
for (var region in $scope.timeZones) {
// Get time zones within that region
var timeZonesInRegion = $scope.timeZones[region];
// For each of those time zones
for (var timeZoneName in timeZonesInRegion) {
// Get corresponding ID
var timeZoneID = timeZonesInRegion[timeZoneName];
// Store region in map
regions[timeZoneID] = region;
}
}
return regions;
})();
/**
* Map of regions to the currently selected time zone for that region.
* Initially, all regions will be set to default selections (the first
* time zone, sorted lexicographically).
*
* @type Object.<String, String>
*/
var selectedTimeZone = (function produceDefaultTimeZones() {
var defaultTimeZone = {};
// For each available region
for (var region in $scope.timeZones) {
// Get time zones within that region
var timeZonesInRegion = $scope.timeZones[region];
// No default initially
var defaultZoneName = null;
var defaultZoneID = null;
// For each of those time zones
for (var timeZoneName in timeZonesInRegion) {
// Get corresponding ID
var timeZoneID = timeZonesInRegion[timeZoneName];
// Set as default if earlier than existing default
if (!defaultZoneName || timeZoneName < defaultZoneName) {
defaultZoneName = timeZoneName;
defaultZoneID = timeZoneID;
}
}
// Store default zone
defaultTimeZone[region] = defaultZoneID;
}
return defaultTimeZone;
})();
/**
* The name of the region currently selected. The selected region narrows
* which time zones are selectable.
*
* @type String
*/
$scope.region = '';
// Ensure corresponding region is selected
$scope.$watch('model', function setModel(model) {
$scope.region = timeZoneRegions[model] || '';
selectedTimeZone[$scope.region] = model;
});
// Restore time zone selection when region changes
$scope.$watch('region', function restoreSelection(region) {
$scope.model = selectedTimeZone[region] || null;
});
}]);

View File

@@ -0,0 +1,255 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive that allows editing of a collection of fields.
*/
angular.module('form').directive('guacForm', [function form() {
return {
// Element only
restrict: 'E',
replace: true,
scope: {
/**
* The translation namespace of the translation strings that will
* be generated for all fields. This namespace is absolutely
* required. If this namespace is omitted, all generated
* translation strings will be placed within the MISSING_NAMESPACE
* namespace, as a warning.
*
* @type String
*/
namespace : '=',
/**
* The form content to display. This may be a form, an array of
* forms, or a simple array of fields.
*
* @type Form[]|Form|Field[]|Field
*/
content : '=',
/**
* The object which will receive all field values. Each field value
* will be assigned to the property of this object having the same
* name.
*
* @type Object.<String, String>
*/
model : '=',
/**
* Whether the contents of the form should be restricted to those
* fields/forms which match properties defined within the given
* model object. By default, all fields will be shown.
*
* @type Boolean
*/
modelOnly : '=',
/**
* Whether the contents of the form should be rendered as disabled.
* By default, form fields are enabled.
*
* @type Boolean
*/
disabled : '=',
/**
* The name of the field to be focused, if any.
*
* @type String
*/
focused : '='
},
templateUrl: 'app/form/templates/form.html',
controller: ['$scope', '$injector', function formController($scope, $injector) {
// Required services
var translationStringService = $injector.get('translationStringService');
/**
* The array of all forms to display.
*
* @type Form[]
*/
$scope.forms = [];
/**
* The object which will receive all field values. Normally, this
* will be the object provided within the "model" attribute. If
* no such object has been provided, a blank model will be used
* instead as a placeholder, such that the fields of this form
* will have something to bind to.
*
* @type Object.<String, String>
*/
$scope.values = {};
/**
* Produces the translation string for the section header of the
* given form. The translation string will be of the form:
*
* <code>NAMESPACE.SECTION_HEADER_NAME<code>
*
* where <code>NAMESPACE</code> is the namespace provided to the
* directive and <code>NAME</code> is the form name transformed
* via translationStringService.canonicalize().
*
* @param {Form} form
* The form for which to produce the translation string.
*
* @returns {String}
* The translation string which produces the translated header
* of the form.
*/
$scope.getSectionHeader = function getSectionHeader(form) {
// If no form, or no name, then no header
if (!form || !form.name)
return '';
return translationStringService.canonicalize($scope.namespace || 'MISSING_NAMESPACE')
+ '.SECTION_HEADER_' + translationStringService.canonicalize(form.name);
};
/**
* Determines whether the given object is a form, under the
* assumption that the object is either a form or a field.
*
* @param {Form|Field} obj
* The object to test.
*
* @returns {Boolean}
* true if the given object appears to be a form, false
* otherwise.
*/
var isForm = function isForm(obj) {
return !!('name' in obj && 'fields' in obj);
};
// Produce set of forms from any given content
$scope.$watch('content', function setContent(content) {
// If no content provided, there are no forms
if (!content) {
$scope.forms = [];
return;
}
// Ensure content is an array
if (!angular.isArray(content))
content = [content];
// If content is an array of fields, convert to an array of forms
if (content.length && !isForm(content[0])) {
content = [{
fields : content
}];
}
// Content is now an array of forms
$scope.forms = content;
});
// Update string value and re-assign to model when field is changed
$scope.$watch('model', function setModel(model) {
// Assign new model only if provided
if (model)
$scope.values = model;
// Otherwise, use blank model
else
$scope.values = {};
});
/**
* Returns whether the given field should be focused or not.
*
* @param {Field} field
* The field to check.
*
* @returns {Boolean}
* true if the given field should be focused, false otherwise.
*/
$scope.isFocused = function isFocused(field) {
return field && (field.name === $scope.focused);
};
/**
* Returns whether the given field should be displayed to the
* current user.
*
* @param {Field} field
* The field to check.
*
* @returns {Boolean}
* true if the given field should be visible, false otherwise.
*/
$scope.isVisible = function isVisible(field) {
// All fields are visible if contents are not restricted to
// model properties only
if (!$scope.modelOnly)
return true;
// Otherwise, fields are only visible if they are present
// within the model
return field && (field.name in $scope.values);
};
/**
* Returns whether at least one of the given fields should be
* displayed to the current user.
*
* @param {Field[]} fields
* The array of fields to check.
*
* @returns {Boolean}
* true if at least one field within the given array should be
* visible, false otherwise.
*/
$scope.containsVisible = function containsVisible(fields) {
// If fields are defined, check whether at least one is visible
if (fields) {
for (var i = 0; i < fields.length; i++) {
if ($scope.isVisible(fields[i]))
return true;
}
}
// Otherwise, there are no visible fields
return false;
};
}] // end controller
};
}]);

View File

@@ -0,0 +1,188 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive that allows editing of a field.
*/
angular.module('form').directive('guacFormField', [function formField() {
return {
// Element only
restrict: 'E',
replace: true,
scope: {
/**
* The translation namespace of the translation strings that will
* be generated for this field. This namespace is absolutely
* required. If this namespace is omitted, all generated
* translation strings will be placed within the MISSING_NAMESPACE
* namespace, as a warning.
*
* @type String
*/
namespace : '=',
/**
* The field to display.
*
* @type Field
*/
field : '=',
/**
* The property which contains this fields current value. When this
* field changes, the property will be updated accordingly.
*
* @type String
*/
model : '=',
/**
* Whether this field should be rendered as disabled. By default,
* form fields are enabled.
*
* @type Boolean
*/
disabled : '=',
/**
* Whether this field should be focused.
*
* @type Boolean
*/
focused : '='
},
templateUrl: 'app/form/templates/formField.html',
controller: ['$scope', '$injector', '$element', function formFieldController($scope, $injector, $element) {
// Required services
var $log = $injector.get('$log');
var formService = $injector.get('formService');
var translationStringService = $injector.get('translationStringService');
/**
* The element which should contain any compiled field content. The
* actual content of a field is dynamically determined by its type.
*
* @type Element[]
*/
var fieldContent = $element.find('.form-field');
/**
* An ID value which is reasonably likely to be unique relative to
* other elements on the page. This ID should be used to associate
* the relevant input element with the label provided by the
* guacFormField directive, if there is such an input element.
*
* @type String
*/
$scope.fieldId = 'guac-field-XXXXXXXXXXXXXXXX'.replace(/X/g, function getRandomCharacter() {
return Math.floor(Math.random() * 36).toString(36);
}) + '-' + new Date().getTime().toString(36);
/**
* Produces the translation string for the header of the current
* field. The translation string will be of the form:
*
* <code>NAMESPACE.FIELD_HEADER_NAME<code>
*
* where <code>NAMESPACE</code> is the namespace provided to the
* directive and <code>NAME</code> is the field name transformed
* via translationStringService.canonicalize().
*
* @returns {String}
* The translation string which produces the translated header
* of the field.
*/
$scope.getFieldHeader = function getFieldHeader() {
// If no field, or no name, then no header
if (!$scope.field || !$scope.field.name)
return '';
return translationStringService.canonicalize($scope.namespace || 'MISSING_NAMESPACE')
+ '.FIELD_HEADER_' + translationStringService.canonicalize($scope.field.name);
};
/**
* Produces the translation string for the given field option
* value. The translation string will be of the form:
*
* <code>NAMESPACE.FIELD_OPTION_NAME_VALUE<code>
*
* where <code>NAMESPACE</code> is the namespace provided to the
* directive, <code>NAME</code> is the field name transformed
* via translationStringService.canonicalize(), and
* <code>VALUE</code> is the option value transformed via
* translationStringService.canonicalize()
*
* @param {String} value
* The name of the option value.
*
* @returns {String}
* The translation string which produces the translated name of the
* value specified.
*/
$scope.getFieldOption = function getFieldOption(value) {
// If no field, or no value, then no corresponding translation string
if (!$scope.field || !$scope.field.name)
return '';
return translationStringService.canonicalize($scope.namespace || 'MISSING_NAMESPACE')
+ '.FIELD_OPTION_' + translationStringService.canonicalize($scope.field.name)
+ '_' + translationStringService.canonicalize(value || 'EMPTY');
};
/**
* Returns whether the current field should be displayed.
*
* @returns {Boolean}
* true if the current field should be displayed, false
* otherwise.
*/
$scope.isFieldVisible = function isFieldVisible() {
return fieldContent[0].hasChildNodes();
};
// Update field contents when field definition is changed
$scope.$watch('field', function setField(field) {
// Reset contents
fieldContent.innerHTML = '';
// Append field content
if (field) {
formService.insertFieldElement(fieldContent[0],
field.type, $scope)['catch'](function fieldCreationFailed() {
$log.warn('Failed to retrieve field with type "' + field.type + '"');
});
}
});
}] // end controller
};
}]);

View File

@@ -0,0 +1,117 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive which implements a color input field. If the underlying color
* picker implementation cannot be used due to a lack of browser support, this
* directive will become read-only, functioning essentially as a color preview.
*
* @see colorPickerService
*/
angular.module('form').directive('guacInputColor', [function guacInputColor() {
var config = {
restrict: 'E',
replace: true,
templateUrl: 'app/form/templates/guacInputColor.html',
transclude: true
};
config.scope = {
/**
* The current selected color value, in standard 6-digit hexadecimal
* RGB notation. When the user selects a different color using this
* directive, this value will updated accordingly.
*
* @type String
*/
model: '=',
/**
* An optional array of colors to include within the color picker as a
* convenient selection of pre-defined colors. The colors within the
* array must be in standard 6-digit hexadecimal RGB notation.
*
* @type String[]
*/
palette: '='
};
config.controller = ['$scope', '$element', '$injector',
function guacInputColorController($scope, $element, $injector) {
// Required services
var colorPickerService = $injector.get('colorPickerService');
/**
* @borrows colorPickerService.isAvailable()
*/
$scope.isColorPickerAvailable = colorPickerService.isAvailable;
/**
* Returns whether the color currently selected is "dark" in the sense
* that the color white will have higher contrast against it than the
* color black.
*
* @returns {Boolean}
* true if the currently selected color is relatively dark (white
* text would provide better contrast than black), false otherwise.
*/
$scope.isDark = function isDark() {
// Assume not dark if color is invalid or undefined
var rgb = $scope.model && /^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/.exec($scope.model);
if (!rgb)
return false;
// Parse color component values as hexadecimal
var red = parseInt(rgb[1], 16);
var green = parseInt(rgb[2], 16);
var blue = parseInt(rgb[3], 16);
// Convert RGB to luminance in HSL space (as defined by the
// relative luminance formula given by the W3C for accessibility)
var luminance = 0.2126 * red + 0.7152 * green + 0.0722 * blue;
// Consider the background to be dark if white text over that
// background would provide better contrast than black
return luminance <= 153; // 153 is the component value 0.6 converted from 0-1 to the 0-255 range
};
/**
* Prompts the user to choose a color by displaying a color selection
* dialog. If the user chooses a color, this directive's model is
* automatically updated. If the user cancels the dialog, the model is
* left untouched.
*/
$scope.selectColor = function selectColor() {
colorPickerService.selectColor($element[0], $scope.model, $scope.palette)
.then(function colorSelected(color) {
$scope.model = color;
}, angular.noop);
};
}];
return config;
}]);

View File

@@ -0,0 +1,80 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive which modifies the parsing and formatting of ngModel when used
* on an HTML5 date input field, relaxing the otherwise strict parsing and
* validation behavior. The behavior of this directive for other input elements
* is undefined.
*/
angular.module('form').directive('guacLenientDate', ['$injector',
function guacLenientDate($injector) {
// Required services
var $filter = $injector.get('$filter');
/**
* Directive configuration object.
*
* @type Object.<String, Object>
*/
var config = {
restrict : 'A',
require : 'ngModel'
};
// Linking function
config.link = function linkGuacLenientDate($scope, $element, $attrs, ngModel) {
// Parse date strings leniently
ngModel.$parsers = [function parse(viewValue) {
// If blank, return null
if (!viewValue)
return null;
// Match basic date pattern
var match = /([0-9]*)(?:-([0-9]*)(?:-([0-9]*))?)?/.exec(viewValue);
if (!match)
return null;
// Determine year, month, and day based on pattern
var year = parseInt(match[1] || '0') || new Date().getFullYear();
var month = parseInt(match[2] || '0') || 1;
var day = parseInt(match[3] || '0') || 1;
// Convert to Date object
var parsedDate = new Date(Date.UTC(year, month - 1, day));
if (isNaN(parsedDate.getTime()))
return null;
return parsedDate;
}];
// Format date strings as "yyyy-MM-dd"
ngModel.$formatters = [function format(modelValue) {
return modelValue ? $filter('date')(modelValue, 'yyyy-MM-dd', 'UTC') : '';
}];
};
return config;
}]);

View File

@@ -0,0 +1,100 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A directive which modifies the parsing and formatting of ngModel when used
* on an HTML5 time input field, relaxing the otherwise strict parsing and
* validation behavior. The behavior of this directive for other input elements
* is undefined.
*/
angular.module('form').directive('guacLenientTime', ['$injector',
function guacLenientTime($injector) {
// Required services
var $filter = $injector.get('$filter');
/**
* Directive configuration object.
*
* @type Object.<String, Object>
*/
var config = {
restrict : 'A',
require : 'ngModel'
};
// Linking function
config.link = function linkGuacLenientTIme($scope, $element, $attrs, ngModel) {
// Parse time strings leniently
ngModel.$parsers = [function parse(viewValue) {
// If blank, return null
if (!viewValue)
return null;
// Match basic time pattern
var match = /([0-9]*)(?::([0-9]*)(?::([0-9]*))?)?(?:\s*(a|p))?/.exec(viewValue.toLowerCase());
if (!match)
return null;
// Determine hour, minute, and second based on pattern
var hour = parseInt(match[1] || '0');
var minute = parseInt(match[2] || '0');
var second = parseInt(match[3] || '0');
// Handle AM/PM
if (match[4]) {
// Interpret 12 AM as 00:00 and 12 PM as 12:00
if (hour === 12)
hour = 0;
// Increment hour to evening if PM
if (match[4] === 'p')
hour += 12;
}
// Wrap seconds and minutes into minutes and hours
minute += second / 60; second %= 60;
hour += minute / 60; minute %= 60;
// Constrain hours to 0 - 23
hour %= 24;
// Convert to Date object
var parsedDate = new Date(Date.UTC(1970, 0, 1, hour, minute, second));
if (isNaN(parsedDate.getTime()))
return null;
return parsedDate;
}];
// Format time strings as "HH:mm:ss"
ngModel.$formatters = [function format(modelValue) {
return modelValue ? $filter('date')(modelValue, 'HH:mm:ss', 'UTC') : '';
}];
};
return config;
}]);

View File

@@ -0,0 +1,26 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Module for displaying dynamic forms.
*/
angular.module('form', [
'locale',
'rest'
]);

View File

@@ -0,0 +1,268 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A service for prompting the user to choose a color using the "Pickr" color
* picker. As the Pickr color picker might not be available if the JavaScript
* features it requires are not supported by the browser (Internet Explorer),
* the isAvailable() function should be used to test for usability.
*/
angular.module('form').provider('colorPickerService', function colorPickerServiceProvider() {
/**
* A singleton instance of the "Pickr" color picker, shared by all users of
* this service. Pickr does not initialize synchronously, nor is it
* supported by all browsers. If Pickr is not yet initialized, or is
* unsupported, this will be null.
*
* @type {Pickr}
*/
var pickr = null;
/**
* Whether Pickr has completed initialization.
*
* @type {Boolean}
*/
var pickrInitComplete = false;
/**
* The HTML element to provide to Pickr as the root element.
*
* @type {HTMLDivElement}
*/
var pickerContainer = document.createElement('div');
pickerContainer.className = 'shared-color-picker';
/**
* An instance of Deferred which represents an active request for the
* user to choose a color. The promise associated with the Deferred will
* be resolved with the chosen color once a color is chosen, and rejected
* if the request is cancelled or Pickr is not available. If no request is
* active, this will be null.
*
* @type {Deferred}
*/
var activeRequest = null;
/**
* Resolves the current active request with the given color value. If no
* color value is provided, the active request is rejected. If no request
* is active, this function has no effect.
*
* @param {String} [color]
* The color value to resolve the active request with.
*/
var completeActiveRequest = function completeActiveRequest(color) {
if (activeRequest) {
// Hide color picker, if shown
pickr.hide();
// Resolve/reject active request depending on value provided
if (color)
activeRequest.resolve(color);
else
activeRequest.reject();
// No active request
activeRequest = null;
}
};
try {
pickr = Pickr.create({
// Bind color picker to the container element
el : pickerContainer,
// Wrap color picker dialog in Guacamole-specific class for
// sake of additional styling
appClass : 'guac-input-color-picker',
'default' : '#000000',
// Display color details as hex
defaultRepresentation : 'HEX',
// Use "monolith" theme, as a nice balance between "nano" (does
// not work in Internet Explorer) and "classic" (too big)
theme : 'monolith',
// Leverage the container element as the button which shows the
// picker, relying on our own styling for that button
useAsButton : true,
appendToBody : true,
// Do not include opacity controls
lockOpacity : true,
// Include a selection of palette entries for convenience and
// reference
swatches : [],
components: {
// Include hue and color preview controls
preview : true,
hue : true,
// Display only a text color input field and the save and
// cancel buttons (no clear button)
interaction: {
input : true,
save : true,
cancel : true
}
}
});
// Hide color picker after user clicks "cancel"
pickr.on('cancel', function colorChangeCanceled() {
completeActiveRequest();
});
// Keep model in sync with changes to the color picker
pickr.on('save', function colorChanged(color) {
completeActiveRequest(color.toHEXA().toString());
activeRequest = null;
});
// Keep color picker in sync with changes to the model
pickr.on('init', function pickrReady() {
pickrInitComplete = true;
});
}
catch (e) {
// If the "Pickr" color picker cannot be loaded (Internet Explorer),
// the available flag will remain set to false
}
// Factory method required by provider
this.$get = ['$injector', function colorPickerServiceFactory($injector) {
// Required services
var $q = $injector.get('$q');
var $translate = $injector.get('$translate');
var service = {};
/**
* Promise which is resolved when Pickr initialization has completed
* and rejected if Pickr cannot be used.
*
* @type {Promise}
*/
var pickrPromise = (function getPickr() {
var deferred = $q.defer();
// Resolve promise when Pickr has completed initialization
if (pickrInitComplete)
deferred.resolve();
else if (pickr)
pickr.on('init', deferred.resolve);
// Reject promise if Pickr cannot be used at all
else
deferred.reject();
return deferred.promise;
})();
/**
* Returns whether the underlying color picker (Pickr) can be used by
* calling selectColor(). If the browser cannot support the color
* picker, false is returned.
*
* @returns {Boolean}
* true if the underlying color picker can be used by calling
* selectColor(), false otherwise.
*/
service.isAvailable = function isAvailable() {
return !!pickr;
};
/**
* Prompts the user to choose a color, returning the color chosen via a
* Promise.
*
* @param {Element} element
* The element that the user interacted with to indicate their
* desire to choose a color.
*
* @param {String} current
* The color that should be selected by default, in standard
* 6-digit hexadecimal RGB format, including "#" prefix.
*
* @param {String[]} [palette]
* An array of color choices which should be exposed to the user
* within the color chooser for convenience. Each color must be in
* standard 6-digit hexadecimal RGB format, including "#" prefix.
*
* @returns {Promise.<String>}
* A Promise which is resolved with the color chosen by the user,
* in standard 6-digit hexadecimal RGB format with "#" prefix, and
* rejected if the selection operation was cancelled or the color
* picker cannot be used.
*/
service.selectColor = function selectColor(element, current, palette) {
// Show picker once the relevant translation strings have been
// retrieved and Pickr is ready for use
return $q.all({
'saveString' : $translate('APP.ACTION_SAVE'),
'cancelString' : $translate('APP.ACTION_CANCEL'),
'pickr' : pickrPromise
}).then(function dependenciesReady(deps) {
// Cancel any active request
completeActiveRequest();
// Reset state of color picker to provided parameters
pickr.setColor(current);
element.appendChild(pickerContainer);
// Assign translated strings to button text
var pickrRoot = pickr.getRoot();
pickrRoot.interaction.save.value = deps.saveString;
pickrRoot.interaction.cancel.value = deps.cancelString;
// Replace all color swatches with the palette of colors given
while (pickr.removeSwatch(0)) {}
angular.forEach(palette, pickr.addSwatch.bind(pickr));
// Show color picker and wait for user to complete selection
activeRequest = $q.defer();
pickr.show();
return activeRequest.promise;
});
};
return service;
}];
});

View File

@@ -0,0 +1,381 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A service for maintaining form-related metadata and linking that data to
* corresponding controllers and templates.
*/
angular.module('form').provider('formService', function formServiceProvider() {
/**
* Reference to the provider itself.
*
* @type formServiceProvider
*/
var provider = this;
/**
* Map of all registered field type definitions by name.
*
* @type Object.<String, FieldType>
*/
this.fieldTypes = {
/**
* Text field type.
*
* @see {@link Field.Type.TEXT}
* @type FieldType
*/
'TEXT' : {
module : 'form',
controller : 'textFieldController',
templateUrl : 'app/form/templates/textField.html'
},
/**
* Email address field type.
*
* @see {@link Field.Type.EMAIL}
* @type FieldType
*/
'EMAIL' : {
templateUrl : 'app/form/templates/emailField.html'
},
/**
* Numeric field type.
*
* @see {@link Field.Type.NUMERIC}
* @type FieldType
*/
'NUMERIC' : {
module : 'form',
controller : 'numberFieldController',
templateUrl : 'app/form/templates/numberField.html'
},
/**
* Boolean field type.
*
* @see {@link Field.Type.BOOLEAN}
* @type FieldType
*/
'BOOLEAN' : {
module : 'form',
controller : 'checkboxFieldController',
templateUrl : 'app/form/templates/checkboxField.html'
},
/**
* Username field type. Identical in principle to a text field, but may
* have different semantics.
*
* @see {@link Field.Type.USERNAME}
* @type FieldType
*/
'USERNAME' : {
templateUrl : 'app/form/templates/textField.html'
},
/**
* Password field type. Similar to a text field, but the contents of
* the field are masked.
*
* @see {@link Field.Type.PASSWORD}
* @type FieldType
*/
'PASSWORD' : {
module : 'form',
controller : 'passwordFieldController',
templateUrl : 'app/form/templates/passwordField.html'
},
/**
* Enumerated field type. The user is presented a finite list of values
* to choose from.
*
* @see {@link Field.Type.ENUM}
* @type FieldType
*/
'ENUM' : {
module : 'form',
controller : 'selectFieldController',
templateUrl : 'app/form/templates/selectField.html'
},
/**
* Multiline field type. The user may enter multiple lines of text.
*
* @see {@link Field.Type.MULTILINE}
* @type FieldType
*/
'MULTILINE' : {
templateUrl : 'app/form/templates/textAreaField.html'
},
/**
* Field type which allows selection of languages. The languages
* displayed are the set of languages supported by the Guacamole web
* application. Legal values are valid language IDs, as dictated by
* the filenames of Guacamole's available translations.
*
* @see {@link Field.Type.LANGUAGE}
* @type FieldType
*/
'LANGUAGE' : {
module : 'form',
controller : 'languageFieldController',
templateUrl : 'app/form/templates/languageField.html'
},
/**
* Field type which allows selection of time zones.
*
* @see {@link Field.Type.TIMEZONE}
* @type FieldType
*/
'TIMEZONE' : {
module : 'form',
controller : 'timeZoneFieldController',
templateUrl : 'app/form/templates/timeZoneField.html'
},
/**
* Field type which allows selection of individual dates.
*
* @see {@link Field.Type.DATE}
* @type FieldType
*/
'DATE' : {
module : 'form',
controller : 'dateFieldController',
templateUrl : 'app/form/templates/dateField.html'
},
/**
* Field type which allows selection of times of day.
*
* @see {@link Field.Type.TIME}
* @type FieldType
*/
'TIME' : {
module : 'form',
controller : 'timeFieldController',
templateUrl : 'app/form/templates/timeField.html'
},
/**
* Field type which allows selection of color schemes accepted by the
* Guacamole server terminal emulator and protocols which leverage it.
*
* @see {@link Field.Type.TERMINAL_COLOR_SCHEME}
* @type FieldType
*/
'TERMINAL_COLOR_SCHEME' : {
module : 'form',
controller : 'terminalColorSchemeFieldController',
templateUrl : 'app/form/templates/terminalColorSchemeField.html'
},
/**
* Field type that supports redirecting the client browser to another
* URL.
*
* @see {@link Field.Type.REDIRECT}
* @type FieldType
*/
'REDIRECT' : {
module : 'form',
controller : 'redirectFieldController',
templateUrl : 'app/form/templates/redirectField.html'
}
};
/**
* Registers a new field type under the given name.
*
* @param {String} fieldTypeName
* The name which uniquely identifies the field type being registered.
*
* @param {FieldType} fieldType
* The field type definition to associate with the given name.
*/
this.registerFieldType = function registerFieldType(fieldTypeName, fieldType) {
// Store field type
provider.fieldTypes[fieldTypeName] = fieldType;
};
// Factory method required by provider
this.$get = ['$injector', function formServiceFactory($injector) {
// Required services
var $compile = $injector.get('$compile');
var $q = $injector.get('$q');
var $templateRequest = $injector.get('$templateRequest');
/**
* Map of module name to the injector instance created for that module.
*
* @type {Object.<String, injector>}
*/
var injectors = {};
var service = {};
service.fieldTypes = provider.fieldTypes;
/**
* Given the name of a module, returns an injector instance which
* injects dependencies within that module. A new injector may be
* created and initialized if no such injector has yet been requested.
* If the injector available to formService already includes the
* requested module, that injector will simply be returned.
*
* @param {String} module
* The name of the module to produce an injector for.
*
* @returns {injector}
* An injector instance which injects dependencies for the given
* module.
*/
var getInjector = function getInjector(module) {
// Use the formService's injector if possible
if ($injector.modules[module])
return $injector;
// If the formService's injector does not include the requested
// module, create the necessary injector, reusing that injector for
// future calls
injectors[module] = injectors[module] || angular.injector(['ng', module]);
return injectors[module];
};
/**
* Compiles and links the field associated with the given name to the given
* scope, producing a distinct and independent DOM Element which functions
* as an instance of that field. The scope object provided must include at
* least the following properties:
*
* namespace:
* A String which defines the unique namespace associated the
* translation strings used by the form using a field of this type.
*
* fieldId:
* A String value which is reasonably likely to be unique and may
* be used to associate the main element of the field with its
* label.
*
* field:
* The Field object that is being rendered, representing a field of
* this type.
*
* model:
* The current String value of the field, if any.
*
* disabled:
* A boolean value which is true if the field should be disabled.
* If false or undefined, the field should be enabled.
*
* @param {Element} fieldContainer
* The DOM Element whose contents should be replaced with the
* compiled field template.
*
* @param {String} fieldTypeName
* The name of the field type defining the nature of the element to be
* created.
*
* @param {Object} scope
* The scope to which the new element will be linked.
*
* @return {Promise.<Element>}
* A Promise which resolves to the compiled Element. If an error occurs
* while retrieving the field type, this Promise will be rejected.
*/
service.insertFieldElement = function insertFieldElement(fieldContainer,
fieldTypeName, scope) {
// Ensure field type is defined
var fieldType = provider.fieldTypes[fieldTypeName];
if (!fieldType)
return $q.reject();
var templateRequest;
// Use raw HTML template if provided
if (fieldType.template) {
var deferredTemplate = $q.defer();
deferredTemplate.resolve(fieldType.template);
templateRequest = deferredTemplate.promise;
}
// If no raw HTML template is provided, retrieve template from URL
else if (fieldType.templateUrl)
templateRequest = $templateRequest(fieldType.templateUrl);
// Otherwise, use empty template
else {
var emptyTemplate= $q.defer();
emptyTemplate.resolve('');
templateRequest = emptyTemplate.promise;
}
// Defer compilation of template pending successful retrieval
var compiledTemplate = $q.defer();
// Resolve with compiled HTML upon success
templateRequest.then(function templateRetrieved(html) {
// Insert template into DOM
fieldContainer.innerHTML = html;
// Populate scope using defined controller
if (fieldType.module && fieldType.controller) {
var $controller = getInjector(fieldType.module).get('$controller');
$controller(fieldType.controller, {
'$scope' : scope,
'$element' : angular.element(fieldContainer.childNodes)
});
}
// Compile DOM with populated scope
compiledTemplate.resolve($compile(fieldContainer.childNodes)(scope));
})
// Reject on failure
['catch'](function templateError() {
compiledTemplate.reject();
});
// Return promise which resolves to the compiled template
return compiledTemplate.promise;
};
return service;
}];
});

View File

@@ -0,0 +1,47 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/* Keep toggle-password icon on same line */
.form-field .password-field {
white-space: nowrap;
}
/* Generic 1x1em icon/button */
.form-field .password-field .icon.toggle-password {
display: inline-block;
opacity: 0.5;
cursor: default;
background-repeat: no-repeat;
background-size: 1em;
width: 1em;
height: 1em;
}
/* Icon for unmasking passwords */
.form-field .password-field input[type=password] ~ .icon.toggle-password {
background-image: url('images/action-icons/guac-show-pass.png');
}
/* Icon for masking passwords */
.form-field .password-field input[type=text] ~ .icon.toggle-password {
background-image: url('images/action-icons/guac-hide-pass.png');
}

View File

@@ -0,0 +1,24 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.form table.fields th {
text-align: left;
font-weight: normal;
padding-right: 1em;
}

View File

@@ -0,0 +1,35 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.redirect-field-container {
height: 100%;
width: 100%;
position: fixed;
left: 0;
top: 0;
display: table;
background: white;
}
.redirect-field {
width: 100%;
display: table-cell;
vertical-align: middle;
text-align: center;
}

View File

@@ -0,0 +1,158 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
.terminal-color-scheme-field {
max-width: 320px;
}
.terminal-color-scheme-field select {
width: 100%;
}
.terminal-color-scheme-field .custom-color-scheme {
background: #EEE;
padding: 0.5em;
border: 1px solid silver;
border-spacing: 0;
margin-top: -2px;
width: 100%;
}
.terminal-color-scheme-field .custom-color-scheme-section {
display: -ms-flexbox;
display: -moz-box;
display: -webkit-box;
display: -webkit-flex;
display: flex;
}
.terminal-color-scheme-field .guac-input-color {
display: block;
margin: 2px;
width: 1.5em;
height: 1.5em;
min-width: 1.25em;
border-radius: 0.15em;
line-height: 1.5em;
text-align: center;
font-size: 0.75em;
cursor: pointer;
color: black;
-ms-flex: 1;
-moz-box-flex: 1;
-webkit-box-flex: 1;
-webkit-flex: 1;
flex: 1;
}
.terminal-color-scheme-field .guac-input-color.read-only {
cursor: not-allowed;
}
.terminal-color-scheme-field .guac-input-color.dark {
color: white;
}
.terminal-color-scheme-field .palette .guac-input-color {
font-weight: bold;
}
/* Hide palette numbers unless color scheme details are visible */
.terminal-color-scheme-field.custom-color-scheme-details-hidden .custom-color-scheme .palette .guac-input-color {
color: transparent;
}
/*
* Custom color scheme details header
*/
.terminal-color-scheme-field .custom-color-scheme-details-header {
font-size: 0.8em;
margin: 0.5em 0;
padding: 0;
}
.terminal-color-scheme-field .custom-color-scheme-details-header::before {
content: '▸ ';
}
.terminal-color-scheme-field.custom-color-scheme-details-visible .custom-color-scheme-details-header::before {
content: '▾ ';
}
/*
* Details show/hide link
*/
/* Render show/hide as a link */
.terminal-color-scheme-field .custom-color-scheme-hide-details,
.terminal-color-scheme-field .custom-color-scheme-show-details {
color: blue;
text-decoration: underline;
cursor: pointer;
margin: 0 0.25em;
font-weight: normal;
}
.terminal-color-scheme-field .custom-color-scheme-hide-details {
display: none;
}
.terminal-color-scheme-field.custom-color-scheme-details-visible .custom-color-scheme-hide-details {
display: inline;
}
.terminal-color-scheme-field.custom-color-scheme-details-visible .custom-color-scheme-show-details {
display: none;
}
/*
* Color scheme details
*/
.terminal-color-scheme-field .custom-color-scheme-details {
display: none;
}
.terminal-color-scheme-field.custom-color-scheme-details-visible .custom-color-scheme-details {
display: block;
width: 100%;
margin: 0.5em 0;
}
/*
* Color picker
*/
/* Increase width of color picker to allow two even rows of eight color
* swatches */
.guac-input-color-picker[data-theme="monolith"] {
width: 16.25em;
}
/* Remove Guacamole-specific styles inherited from the generic button rules */
.guac-input-color-picker[data-theme="monolith"] button {
min-width: 0;
padding: 0;
margin: 0;
box-shadow: none;
}

View File

@@ -0,0 +1,7 @@
<input type="checkbox"
ng-attr-id="{{ fieldId }}"
ng-disabled="disabled"
ng-model="typedValue"
guac-focus="focused"
autocorrect="off"
autocapitalize="off"/>

Some files were not shown because too many files have changed in this diff Show More