mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-08 06:01:22 +00:00
GUACAMOLE-773: Migrate to NPM for AngularJS portion of webapp build.
This commit is contained in:
4
guacamole/src/main/frontend/.gitignore
vendored
Normal file
4
guacamole/src/main/frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
*~
|
||||
node_modules
|
||||
dist
|
||||
generated
|
34
guacamole/src/main/frontend/externs.js
Normal file
34
guacamole/src/main/frontend/externs.js
Normal 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) {}
|
||||
|
105
guacamole/src/main/frontend/gulpfile.js
Normal file
105
guacamole/src/main/frontend/gulpfile.js
Normal 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')
|
||||
));
|
||||
|
6808
guacamole/src/main/frontend/package-lock.json
generated
Normal file
6808
guacamole/src/main/frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
36
guacamole/src/main/frontend/package.json
Normal file
36
guacamole/src/main/frontend/package.json
Normal 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"
|
||||
}
|
||||
}
|
26
guacamole/src/main/frontend/src/app/auth/authModule.js
Normal file
26
guacamole/src/main/frontend/src/app/auth/authModule.js
Normal 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'
|
||||
]);
|
@@ -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;
|
||||
}]);
|
@@ -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;
|
||||
|
||||
}]);
|
34
guacamole/src/main/frontend/src/app/client/clientModule.js
Normal file
34
guacamole/src/main/frontend/src/app/client/clientModule.js
Normal 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
@@ -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
|
||||
*/
|
||||
|
||||
}]
|
||||
};
|
||||
}]);
|
@@ -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();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}]
|
||||
};
|
||||
}]);
|
@@ -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);
|
||||
|
||||
});
|
||||
|
||||
}]
|
||||
|
||||
};
|
||||
}]);
|
@@ -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
|
||||
|
||||
};
|
||||
}]);
|
@@ -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);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
}]
|
||||
|
||||
};
|
||||
}]);
|
@@ -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);
|
||||
|
||||
});
|
||||
|
||||
}]
|
||||
};
|
||||
}]);
|
@@ -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);
|
||||
});
|
||||
|
||||
}]
|
||||
};
|
||||
}]);
|
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
@@ -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();
|
||||
|
||||
})();
|
||||
|
||||
}]);
|
@@ -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;
|
||||
|
||||
}]);
|
135
guacamole/src/main/frontend/src/app/client/services/guacImage.js
Normal file
135
guacamole/src/main/frontend/src/app/client/services/guacImage.js
Normal 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;
|
||||
|
||||
}]);
|
@@ -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();
|
||||
|
||||
})();
|
||||
|
||||
}]);
|
129
guacamole/src/main/frontend/src/app/client/styles/client.css
Normal file
129
guacamole/src/main/frontend/src/app/client/styles/client.css
Normal 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');
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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');
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
||||
}
|
208
guacamole/src/main/frontend/src/app/client/styles/guac-menu.css
Normal file
208
guacamole/src/main/frontend/src/app/client/styles/guac-menu.css
Normal 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;
|
||||
|
||||
}
|
@@ -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;
|
||||
|
||||
}
|
167
guacamole/src/main/frontend/src/app/client/styles/menu.css
Normal file
167
guacamole/src/main/frontend/src/app/client/styles/menu.css
Normal 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;
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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');
|
||||
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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%;
|
||||
}
|
132
guacamole/src/main/frontend/src/app/client/styles/transfer.css
Normal file
132
guacamole/src/main/frontend/src/app/client/styles/transfer.css
Normal 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%;
|
||||
}
|
@@ -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;
|
||||
}
|
235
guacamole/src/main/frontend/src/app/client/templates/client.html
Normal file
235
guacamole/src/main/frontend/src/app/client/templates/client.html
Normal 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>
|
@@ -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>
|
@@ -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>
|
@@ -0,0 +1,9 @@
|
||||
<div class="list-item">
|
||||
|
||||
<!-- Filename and icon -->
|
||||
<div class="caption">
|
||||
<div class="icon"></div>
|
||||
{{::name}}
|
||||
</div>
|
||||
|
||||
</div>
|
@@ -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>
|
@@ -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>
|
@@ -0,0 +1,6 @@
|
||||
<div class="file-browser">
|
||||
|
||||
<!-- Current directory contents -->
|
||||
<div class="current-directory-contents"></div>
|
||||
|
||||
</div>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -0,0 +1,2 @@
|
||||
<div class="viewport" ng-transclude>
|
||||
</div>
|
@@ -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;
|
||||
|
||||
}]);
|
@@ -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;
|
||||
|
||||
}]);
|
@@ -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;
|
||||
|
||||
}]);
|
@@ -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;
|
||||
|
||||
}]);
|
@@ -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;
|
||||
|
||||
}]);
|
@@ -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;
|
||||
|
||||
}]);
|
@@ -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;
|
||||
|
||||
}]);
|
@@ -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;
|
||||
|
||||
}]);
|
@@ -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;
|
||||
|
||||
}]);
|
@@ -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;
|
||||
|
||||
}]);
|
@@ -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', []);
|
@@ -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;
|
||||
|
||||
}]);
|
@@ -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;
|
||||
|
||||
}]);
|
@@ -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;
|
||||
}
|
@@ -0,0 +1 @@
|
||||
<textarea class="clipboard"></textarea>
|
@@ -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;
|
||||
|
||||
}]);
|
@@ -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
|
||||
|
||||
};
|
||||
|
||||
}]);
|
@@ -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));
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}]);
|
@@ -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
|
||||
|
||||
};
|
||||
|
||||
}]);
|
@@ -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
|
||||
|
||||
};
|
||||
|
||||
}]);
|
@@ -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
|
||||
|
||||
};
|
||||
|
||||
}]);
|
24
guacamole/src/main/frontend/src/app/element/elementModule.js
Normal file
24
guacamole/src/main/frontend/src/app/element/elementModule.js
Normal 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', []);
|
@@ -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;
|
||||
}
|
@@ -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>
|
47
guacamole/src/main/frontend/src/app/element/types/Marker.js
Normal file
47
guacamole/src/main/frontend/src/app/element/types/Marker.js
Normal 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;
|
||||
|
||||
}]);
|
@@ -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;
|
||||
|
||||
}]);
|
@@ -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] : '');
|
||||
});
|
||||
|
||||
}]);
|
@@ -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') : '');
|
||||
});
|
||||
|
||||
}]);
|
@@ -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 = '';
|
||||
});
|
||||
|
||||
}]);
|
@@ -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() : '');
|
||||
});
|
||||
|
||||
}]);
|
@@ -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';
|
||||
|
||||
};
|
||||
|
||||
}]);
|
@@ -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;
|
||||
|
||||
}]);
|
@@ -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 = '';
|
||||
});
|
||||
|
||||
}]);
|
@@ -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);
|
||||
|
||||
}]);
|
@@ -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';
|
||||
|
||||
}]);
|
@@ -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') : '');
|
||||
});
|
||||
|
||||
}]);
|
@@ -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;
|
||||
});
|
||||
|
||||
}]);
|
255
guacamole/src/main/frontend/src/app/form/directives/form.js
Normal file
255
guacamole/src/main/frontend/src/app/form/directives/form.js
Normal 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
|
||||
};
|
||||
|
||||
}]);
|
188
guacamole/src/main/frontend/src/app/form/directives/formField.js
Normal file
188
guacamole/src/main/frontend/src/app/form/directives/formField.js
Normal 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
|
||||
};
|
||||
|
||||
}]);
|
@@ -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;
|
||||
|
||||
}]);
|
@@ -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;
|
||||
|
||||
}]);
|
@@ -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;
|
||||
|
||||
}]);
|
26
guacamole/src/main/frontend/src/app/form/formModule.js
Normal file
26
guacamole/src/main/frontend/src/app/form/formModule.js
Normal 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'
|
||||
]);
|
@@ -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;
|
||||
|
||||
}];
|
||||
|
||||
});
|
381
guacamole/src/main/frontend/src/app/form/services/formService.js
Normal file
381
guacamole/src/main/frontend/src/app/form/services/formService.js
Normal 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;
|
||||
|
||||
}];
|
||||
|
||||
});
|
@@ -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');
|
||||
}
|
24
guacamole/src/main/frontend/src/app/form/styles/form.css
Normal file
24
guacamole/src/main/frontend/src/app/form/styles/form.css
Normal 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;
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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
Reference in New Issue
Block a user