diff --git a/guacamole/src/main/frontend/src/app/element/directives/guacDrop.js b/guacamole/src/main/frontend/src/app/element/directives/guacDrop.js new file mode 100644 index 000000000..3f7070694 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/element/directives/guacDrop.js @@ -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 + + }; + +}]); diff --git a/guacamole/src/main/frontend/src/app/element/directives/guacUpload.js b/guacamole/src/main/frontend/src/app/element/directives/guacUpload.js index b7d75e95d..d1c10a9da 100644 --- a/guacamole/src/main/frontend/src/app/element/directives/guacUpload.js +++ b/guacamole/src/main/frontend/src/app/element/directives/guacUpload.js @@ -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); diff --git a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js index 1ed354698..993a8e534 100644 --- a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js +++ b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js @@ -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; - - }); - }]); diff --git a/guacamole/src/main/frontend/src/app/import/directives/connectionImportErrors.js b/guacamole/src/main/frontend/src/app/import/directives/connectionImportErrors.js index aae51db63..d6a14c45e 100644 --- a/guacamole/src/main/frontend/src/app/import/directives/connectionImportErrors.js +++ b/guacamole/src/main/frontend/src/app/import/directives/connectionImportErrors.js @@ -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 diff --git a/guacamole/src/main/frontend/src/app/import/importModule.js b/guacamole/src/main/frontend/src/app/import/importModule.js index 164ec2b16..45551e323 100644 --- a/guacamole/src/main/frontend/src/app/import/importModule.js +++ b/guacamole/src/main/frontend/src/app/import/importModule.js @@ -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']); diff --git a/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html b/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html index ae937a200..bc2ecc2a1 100644 --- a/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html +++ b/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html @@ -22,7 +22,10 @@ -