GUACAMOLE-926: Factor file input and drag/drop out into directives.

This commit is contained in:
James Muehlner
2023-04-07 22:00:09 +00:00
parent 45dc611ab1
commit b831d0a82b
7 changed files with 211 additions and 157 deletions

View File

@@ -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
};
}]);

View File

@@ -18,9 +18,9 @@
*/ */
/** /**
* A directive which allows multiple files to be uploaded. Clicking on the * A directive which allows files to be uploaded. Clicking on the associated
* associated element will result in a file selector dialog, which then calls * element will result in a file selector dialog, which then calls the provided
* the provided callback function with any chosen files. * callback function with any chosen files.
*/ */
angular.module('element').directive('guacUpload', ['$document', function guacUpload($document) { angular.module('element').directive('guacUpload', ['$document', function guacUpload($document) {
@@ -36,32 +36,43 @@ angular.module('element').directive('guacUpload', ['$document', function guacUpl
* *
* @type Function * @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 * @type Element
*/ */
var element = $element[0]; const element = $element[0];
/** /**
* Internal form, containing a single file input element. * Internal form, containing a single file input element.
* *
* @type HTMLFormElement * @type HTMLFormElement
*/ */
var form = $document[0].createElement('form'); const form = $document[0].createElement('form');
/** /**
* Internal file input element. * Internal file input element.
* *
* @type HTMLInputElement * @type HTMLInputElement
*/ */
var input = $document[0].createElement('input'); const input = $document[0].createElement('input');
// Init input element // Init input element
input.type = 'file'; input.type = 'file';
input.multiple = true; input.multiple = guacMultiple;
// Add input element to internal form // Add input element to internal form
form.appendChild(input); form.appendChild(input);

View File

@@ -62,12 +62,11 @@ const LEGAL_MIME_TYPES = [CSV_MIME_TYPE, JSON_MIME_TYPE, ...YAML_MIME_TYPES];
*/ */
angular.module('import').controller('importConnectionsController', ['$scope', '$injector', angular.module('import').controller('importConnectionsController', ['$scope', '$injector',
function importConnectionsController($scope, $injector) { function importConnectionsController($scope, $injector) {
// Required services // Required services
const $document = $injector.get('$document');
const $location = $injector.get('$location'); const $location = $injector.get('$location');
const $q = $injector.get('$q'); const $q = $injector.get('$q');
const $routeParams = $injector.get('$routeParams'); const $routeParams = $injector.get('$routeParams');
const $timeout = $injector.get('$timeout');
const connectionParseService = $injector.get('connectionParseService'); const connectionParseService = $injector.get('connectionParseService');
const connectionService = $injector.get('connectionService'); const connectionService = $injector.get('connectionService');
const guacNotification = $injector.get('guacNotification'); 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 * 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 * @argument {File[]} files
* The file to upload onto the scope for further processing. * 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 // The MIME type of the provided file
const mimeType = file.type; const mimeType = file.type;
@@ -650,147 +656,10 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$
$scope.fileReader.readAsBinaryString(file); $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 * The name of the file that's currently being uploaded, or has yet to
* be imported, if any. * be imported, if any.
*/ */
$scope.fileName = null; $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;
});
}]); }]);

View File

@@ -161,7 +161,7 @@ angular.module('import').directive('connectionImportErrors', [
// Set up the list of connection errors based on the existing parse // Set up the list of connection errors based on the existing parse
// result, with error messages fetched from the patch failure // result, with error messages fetched from the patch failure
$scope.connectionErrors = parseResult.patches.map( const connectionErrors = parseResult.patches.map(
(patch, index) => { (patch, index) => {
// Generate a connection error for display // Generate a connection error for display

View File

@@ -21,4 +21,4 @@
* The module for code supporting importing user-supplied files. Currently, only * The module for code supporting importing user-supplied files. Currently, only
* connection import is supported. * connection import is supported.
*/ */
angular.module('import', ['rest', 'list', 'notification']); angular.module('import', ['element', 'list', 'notification', 'rest']);

View File

@@ -22,7 +22,10 @@
</a> </a>
</div> </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> <div class="title">{{'IMPORT.HELP_UPLOAD_DROP_TITLE' | translate}}</div>

View File

@@ -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_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_BLANK" : "Your password cannot be blank.",
"ERROR_PASSWORD_MISMATCH" : "The provided passwords do not match.", "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" : "Password:",
"FIELD_HEADER_PASSWORD_AGAIN" : "Re-enter 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_ARRAY_REQUIRED": "The provided file must contain a list of connections",
"ERROR_DUPLICATE_CSV_HEADER": "Duplicate CSV Header: {HEADER}", "ERROR_DUPLICATE_CSV_HEADER": "Duplicate CSV Header: {HEADER}",
"ERROR_EMPTY_FILE": "The provided file is empty", "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_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_GROUP": "No group matching \"{GROUP}\" found",
"ERROR_INVALID_USER_GROUP_IDENTIFIERS": "User Groups not found: {IDENTIFIER_LIST}", "ERROR_INVALID_USER_GROUP_IDENTIFIERS": "User Groups not found: {IDENTIFIER_LIST}",
"ERROR_INVALID_USER_IDENTIFIERS": "Users not found: {IDENTIFIER_LIST}", "ERROR_INVALID_USER_IDENTIFIERS": "Users not found: {IDENTIFIER_LIST}",