mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 13:17:41 +00:00
GUACAMOLE-926: Factor file input and drag/drop out into directives.
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* 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. Dragging files onto
|
||||
* the associated element will call the provided callback function with any
|
||||
* dragged files.
|
||||
*/
|
||||
angular.module('element').directive('guacDrop', ['$injector', function guacDrop($injector) {
|
||||
|
||||
// Required services
|
||||
const guacNotification = $injector.get('guacNotification');
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
|
||||
link: function linkGuacDrop($scope, $element, $attrs) {
|
||||
|
||||
/**
|
||||
* The function to call whenever files are dragged. The callback is
|
||||
* provided a single parameter: the FileList containing all dragged
|
||||
* files.
|
||||
*
|
||||
* @type Function
|
||||
*/
|
||||
const guacDrop = $scope.$eval($attrs.guacDrop);
|
||||
|
||||
/**
|
||||
* Any number of space-seperated classes to be applied to the
|
||||
* element a drop is pending: when the user has dragged something
|
||||
* over the element, but not yet dropped. These classes will be
|
||||
* removed when a drop is not pending.
|
||||
*
|
||||
* @type String
|
||||
*/
|
||||
const guacDraggedClass = $scope.$eval($attrs.guacDraggedClass);
|
||||
|
||||
/**
|
||||
* Whether upload of multiple files should be allowed. If false, an
|
||||
* error will be displayed explaining the restriction, otherwise
|
||||
* any number of files may be dragged. Defaults to true if not set.
|
||||
*
|
||||
* @type Boolean
|
||||
*/
|
||||
const guacMultiple = 'guacMultiple' in $attrs
|
||||
? $scope.$eval($attrs.guacMultiple) : true;
|
||||
|
||||
/**
|
||||
* The element which will register drag event.
|
||||
*
|
||||
* @type Element
|
||||
*/
|
||||
const element = $element[0];
|
||||
|
||||
/**
|
||||
* Applies any classes provided in the guacDraggedClass attribute.
|
||||
* Further propagation and default behavior of the given event is
|
||||
* automatically prevented.
|
||||
*
|
||||
* @param {Event} e
|
||||
* The event related to the in-progress drag/drop operation.
|
||||
*/
|
||||
const notifyDragStart = function notifyDragStart(e) {
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Skip further processing if no classes were provided
|
||||
if (!guacDraggedClass)
|
||||
return;
|
||||
|
||||
// Add each provided class
|
||||
guacDraggedClass.split(' ').forEach(classToApply =>
|
||||
element.classList.add(classToApply));
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes any classes provided in the guacDraggedClass attribute.
|
||||
* Further propagation and default behavior of the given event is
|
||||
* automatically prevented.
|
||||
*
|
||||
* @param {Event} e
|
||||
* The event related to the end of the drag/drop operation.
|
||||
*/
|
||||
const notifyDragEnd = function notifyDragEnd(e) {
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Skip further processing if no classes were provided
|
||||
if (!guacDraggedClass)
|
||||
return;
|
||||
|
||||
// Remove each provided class
|
||||
guacDraggedClass.split(' ').forEach(classToRemove =>
|
||||
element.classList.remove(classToRemove));
|
||||
|
||||
};
|
||||
|
||||
// Add listeners to the drop target to ensure that the visual state
|
||||
// stays up to date
|
||||
element.addEventListener('dragenter', notifyDragStart);
|
||||
element.addEventListener('dragover', notifyDragStart);
|
||||
element.addEventListener('dragleave', notifyDragEnd);
|
||||
|
||||
/**
|
||||
* Event listener that will be invoked if the user drops anything
|
||||
* onto the event. If a valid file is provided, the onFile callback
|
||||
* provided to this directive will be called; otherwise an error
|
||||
* will be displayed, if appropriate.
|
||||
*
|
||||
* @param {Event} e
|
||||
* The drop event that triggered this handler.
|
||||
*/
|
||||
element.addEventListener('drop', e => {
|
||||
|
||||
notifyDragEnd(e);
|
||||
|
||||
const files = e.dataTransfer.files;
|
||||
|
||||
// Ignore any non-files that are dragged into the drop area
|
||||
if (files.length < 1)
|
||||
return;
|
||||
|
||||
// If multi-file upload is disabled, If more than one file was
|
||||
// provided, print an error explaining the problem
|
||||
if (!guacMultiple && files.length >= 2) {
|
||||
|
||||
guacNotification.showStatus({
|
||||
className : 'error',
|
||||
title : 'APP.DIALOG_HEADER_ERROR',
|
||||
text: { key : 'APP.ERROR_SINGLE_FILE_ONLY'},
|
||||
|
||||
// Add a button to hide the error
|
||||
actions : [{
|
||||
name : 'APP.ACTION_ACKNOWLEDGE',
|
||||
callback : () => guacNotification.showStatus(false)
|
||||
}]
|
||||
});
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
// Invoke the callback with the files. Note that if guacMultiple
|
||||
// is set to false, this will always be a single file.
|
||||
guacDrop(files);
|
||||
|
||||
});
|
||||
|
||||
} // end guacDrop link function
|
||||
|
||||
};
|
||||
|
||||
}]);
|
@@ -18,9 +18,9 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* A directive which allows 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) {
|
||||
|
||||
@@ -36,32 +36,43 @@ angular.module('element').directive('guacUpload', ['$document', function guacUpl
|
||||
*
|
||||
* @type Function
|
||||
*/
|
||||
var guacUpload = $scope.$eval($attrs.guacUpload);
|
||||
const guacUpload = $scope.$eval($attrs.guacUpload);
|
||||
|
||||
/**
|
||||
* The element which will register the drag gesture.
|
||||
* Whether upload of multiple files should be allowed. If false, the
|
||||
* file dialog will only allow a single file to be chosen at once,
|
||||
* otherwise any number of files may be chosen. Defaults to true if
|
||||
* not set.
|
||||
*
|
||||
* @type Boolean
|
||||
*/
|
||||
const guacMultiple = 'guacMultiple' in $attrs
|
||||
? $scope.$eval($attrs.guacMultiple) : true;
|
||||
|
||||
/**
|
||||
* The element which will register the click.
|
||||
*
|
||||
* @type Element
|
||||
*/
|
||||
var element = $element[0];
|
||||
const element = $element[0];
|
||||
|
||||
/**
|
||||
* Internal form, containing a single file input element.
|
||||
*
|
||||
* @type HTMLFormElement
|
||||
*/
|
||||
var form = $document[0].createElement('form');
|
||||
const form = $document[0].createElement('form');
|
||||
|
||||
/**
|
||||
* Internal file input element.
|
||||
*
|
||||
* @type HTMLInputElement
|
||||
*/
|
||||
var input = $document[0].createElement('input');
|
||||
const input = $document[0].createElement('input');
|
||||
|
||||
// Init input element
|
||||
input.type = 'file';
|
||||
input.multiple = true;
|
||||
input.multiple = guacMultiple;
|
||||
|
||||
// Add input element to internal form
|
||||
form.appendChild(input);
|
||||
|
@@ -62,12 +62,11 @@ const LEGAL_MIME_TYPES = [CSV_MIME_TYPE, JSON_MIME_TYPE, ...YAML_MIME_TYPES];
|
||||
*/
|
||||
angular.module('import').controller('importConnectionsController', ['$scope', '$injector',
|
||||
function importConnectionsController($scope, $injector) {
|
||||
|
||||
// Required services
|
||||
const $document = $injector.get('$document');
|
||||
const $location = $injector.get('$location');
|
||||
const $q = $injector.get('$q');
|
||||
const $routeParams = $injector.get('$routeParams');
|
||||
const $timeout = $injector.get('$timeout');
|
||||
const connectionParseService = $injector.get('connectionParseService');
|
||||
const connectionService = $injector.get('connectionService');
|
||||
const guacNotification = $injector.get('guacNotification');
|
||||
@@ -586,12 +585,19 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$
|
||||
|
||||
/**
|
||||
* Handle a provided File upload, reading all data onto the scope for
|
||||
* import processing, should the user request an import.
|
||||
* import processing, should the user request an import. Note that this
|
||||
* function is used as a callback for directives that invoke it with a file
|
||||
* list, but directive-level checking should ensure that there is only ever
|
||||
* one file provided at a time.
|
||||
*
|
||||
* @argument {File} file
|
||||
* The file to upload onto the scope for further processing.
|
||||
* @argument {File[]} files
|
||||
* The files to upload onto the scope for further processing. There
|
||||
* should only ever be a single file in the array.
|
||||
*/
|
||||
const handleFile = file => {
|
||||
$scope.handleFiles = files => {
|
||||
|
||||
// There should only ever be a single file in the array
|
||||
const file = files[0];
|
||||
|
||||
// The MIME type of the provided file
|
||||
const mimeType = file.type;
|
||||
@@ -650,147 +656,10 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$
|
||||
$scope.fileReader.readAsBinaryString(file);
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether a drag/drop operation is currently in progress (the user has
|
||||
* dragged a file over the Guacamole connection but has not yet
|
||||
* dropped it).
|
||||
*
|
||||
* @type boolean
|
||||
*/
|
||||
$scope.dropPending = false;
|
||||
|
||||
/**
|
||||
* The name of the file that's currently being uploaded, or has yet to
|
||||
* be imported, if any.
|
||||
*/
|
||||
$scope.fileName = null;
|
||||
|
||||
/**
|
||||
* The container for the file upload UI.
|
||||
*
|
||||
* @type Element
|
||||
*
|
||||
*/
|
||||
const uploadContainer = angular.element(
|
||||
$document.find('.file-upload-container'));
|
||||
|
||||
/**
|
||||
* The location where files can be dragged-and-dropped to.
|
||||
*
|
||||
* @type Element
|
||||
*/
|
||||
const dropTarget = uploadContainer.find('.drop-target');
|
||||
|
||||
/**
|
||||
* Displays a visual indication that dropping the file currently
|
||||
* being dragged is possible. Further propagation and default behavior
|
||||
* of the given event is automatically prevented.
|
||||
*
|
||||
* @param {Event} e
|
||||
* The event related to the in-progress drag/drop operation.
|
||||
*/
|
||||
const notifyDragStart = function notifyDragStart(e) {
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
$scope.$apply(() => {
|
||||
$scope.dropPending = true;
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes the visual indication that dropping the file currently
|
||||
* being dragged is possible. Further propagation and default behavior
|
||||
* of the given event is automatically prevented.
|
||||
*
|
||||
* @param {Event} e
|
||||
* The event related to the end of the former drag/drop operation.
|
||||
*/
|
||||
const notifyDragEnd = function notifyDragEnd(e) {
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
$scope.$apply(() => {
|
||||
$scope.dropPending = false;
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
// Add listeners to the drop target to ensure that the visual state
|
||||
// stays up to date
|
||||
dropTarget.on('dragenter', notifyDragStart);
|
||||
dropTarget.on('dragover', notifyDragStart);
|
||||
dropTarget.on('dragleave', notifyDragEnd);
|
||||
|
||||
/**
|
||||
* Drop target event listener that will be invoked if the user drops
|
||||
* anything onto the drop target. If a valid file is provided, the
|
||||
* onFile callback provided to this directive will be called; otherwise
|
||||
* an error will be displayed, if appropriate.
|
||||
*
|
||||
* @param {Event} e
|
||||
* The drop event that triggered this handler.
|
||||
*/
|
||||
dropTarget.on('drop', e => {
|
||||
|
||||
notifyDragEnd(e);
|
||||
|
||||
const files = e.originalEvent.dataTransfer.files;
|
||||
|
||||
// Ignore any non-files that are dragged into the drop area
|
||||
if (files.length < 1)
|
||||
return;
|
||||
|
||||
if (files.length >= 2) {
|
||||
|
||||
// If more than one file was provided, print an error explaining
|
||||
// that only a single file is allowed and abort processing
|
||||
handleError(new ParseError({
|
||||
message: 'Only a single file may be imported at once',
|
||||
key: 'IMPORT.ERROR_FILE_SINGLE_ONLY'
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
handleFile(files[0]);
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* The hidden file input used to create a file browser.
|
||||
*
|
||||
* @type Element
|
||||
*/
|
||||
const fileUploadInput = uploadContainer.find('.file-upload-input');
|
||||
|
||||
/**
|
||||
* A function that will click on the hidden file input to open a file
|
||||
* browser to allow the user to select a file for upload.
|
||||
*/
|
||||
$scope.openFileBrowser = () =>
|
||||
$timeout(() => fileUploadInput.click(), 0, false);
|
||||
|
||||
/**
|
||||
* A handler that will be invoked when a user selectes a file in the
|
||||
* file browser. After some error checking, the file will be passed to
|
||||
* the onFile callback provided to this directive.
|
||||
*
|
||||
* @param {Event} e
|
||||
* The event that was triggered when the user selected a file in
|
||||
* their file browser.
|
||||
*/
|
||||
fileUploadInput.on('change', e => {
|
||||
|
||||
// Process the uploaded file
|
||||
handleFile(e.target.files[0]);
|
||||
|
||||
// Clear the value to ensure that the change event will be fired
|
||||
// if the user selects the same file again
|
||||
fileUploadInput.value = null;
|
||||
|
||||
});
|
||||
|
||||
}]);
|
||||
|
@@ -161,7 +161,7 @@ angular.module('import').directive('connectionImportErrors', [
|
||||
|
||||
// Set up the list of connection errors based on the existing parse
|
||||
// result, with error messages fetched from the patch failure
|
||||
$scope.connectionErrors = parseResult.patches.map(
|
||||
const connectionErrors = parseResult.patches.map(
|
||||
(patch, index) => {
|
||||
|
||||
// Generate a connection error for display
|
||||
|
@@ -21,4 +21,4 @@
|
||||
* The module for code supporting importing user-supplied files. Currently, only
|
||||
* connection import is supported.
|
||||
*/
|
||||
angular.module('import', ['rest', 'list', 'notification']);
|
||||
angular.module('import', ['element', 'list', 'notification', 'rest']);
|
||||
|
@@ -22,7 +22,10 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="drop-target" ng-class="{ 'drop-pending': dropPending, 'file-present': fileName}">
|
||||
<div class="drop-target" guac-upload="handleFiles"
|
||||
guac-drop="handleFiles" guac-multiple="false"
|
||||
guac-dragged-class="'drop-pending'"
|
||||
ng-class="{'file-present': fileName}">
|
||||
|
||||
<div class="title">{{'IMPORT.HELP_UPLOAD_DROP_TITLE' | translate}}</div>
|
||||
|
||||
|
@@ -41,6 +41,7 @@
|
||||
"ERROR_PAGE_UNAVAILABLE" : "An error has occurred and this action cannot be completed. If the problem persists, please notify your system administrator or check your system logs.",
|
||||
"ERROR_PASSWORD_BLANK" : "Your password cannot be blank.",
|
||||
"ERROR_PASSWORD_MISMATCH" : "The provided passwords do not match.",
|
||||
"ERROR_SINGLE_FILE_ONLY" : "Please upload only a single file at a time",
|
||||
|
||||
"FIELD_HEADER_PASSWORD" : "Password:",
|
||||
"FIELD_HEADER_PASSWORD_AGAIN" : "Re-enter Password:",
|
||||
@@ -204,9 +205,8 @@
|
||||
"ERROR_ARRAY_REQUIRED": "The provided file must contain a list of connections",
|
||||
"ERROR_DUPLICATE_CSV_HEADER": "Duplicate CSV Header: {HEADER}",
|
||||
"ERROR_EMPTY_FILE": "The provided file is empty",
|
||||
"ERROR_FILE_SINGLE_ONLY": "Please upload only a single file at a time",
|
||||
"ERROR_INVALID_CSV_HEADER": "Invalid CSV Header \"{HEADER}\" is neither an attribute or parameter",
|
||||
"ERROR_INVALID_FILE_TYPE": "Unsupported file type: \"{TYPE}\"",
|
||||
"ERROR_INVALID_MIME_TYPE": "Unsupported file type: \"{TYPE}\"",
|
||||
"ERROR_INVALID_GROUP": "No group matching \"{GROUP}\" found",
|
||||
"ERROR_INVALID_USER_GROUP_IDENTIFIERS": "User Groups not found: {IDENTIFIER_LIST}",
|
||||
"ERROR_INVALID_USER_IDENTIFIERS": "Users not found: {IDENTIFIER_LIST}",
|
||||
|
Reference in New Issue
Block a user