Sab. Apr 19th, 2025

Quando si vuole offrire all’utente la possibilità di caricare più file di grandi dimensioni in maniera semplice, è comodo utilizzare un’area di drag & drop personalizzata e un dialog che copra l’intera schermata, così da indicare chiaramente lo stato del caricamento. In Vaadin 24 esiste il componente Upload, che può essere nascosto e controllato manualmente in JavaScript, sfruttando al tempo stesso i metodi server-side di Vaadin per gestire correttamente la ricezione dei file.

In questo esempio si parte da un layout che occupa l’intero schermo, impostato con setSizeFull. Dentro la vista, si istanzia un componente Upload collegato a MultiFileMemoryBuffer, ma gli si impone uno stile display: none. Grazie a questa impostazione, Upload diventa invisibile e non interferisce con l’interfaccia personalizzata. Per permettere all’utente di trascinare i file, si crea un VerticalLayout con uno stile che gli conferisce un bordo tratteggiato, degli angoli arrotondati e del padding per renderlo gradevole. Il layout funge così da contenitore visibile su cui trascinare i file.

All’interno di questo contenitore viene iniettato uno script JavaScript che gestisce gli eventi di dragover, dragleave e drop. Nella fase di dragover, si rende più evidente il bordo tratteggiato, magari aumentandone lo spessore e aggiungendo un’ombra. In dragleave, si rimuovono questi effetti per riportare lo stile alla normalità. Quando l’utente rilascia i file con drop, si modifica di nuovo lo stile per togliere l’effetto di evidenziazione e si assegnano i file trascinati all’input interno del componente Upload nascosto. Questi file vengono passati a Upload forzando la sua logica interna con uploadEl._onFileInputChange, seguita dalla chiamata a uploadEl.uploadFiles, cosicché Vaadin faccia partire in modo ufficiale il caricamento.

Mentre i file vengono caricati, il web component <vaadin-upload> emette eventi lato client, come upload-start, upload-progress, upload-success e upload-error. È in questi eventi che si mostra o si aggiorna una finestra modale creata dinamicamente con JavaScript e posizionata in overlay a schermo intero, così da coprire visivamente qualsiasi altro elemento della pagina. In upload-start, si crea il div a tutto schermo e lo si riempie con un messaggio iniziale. Con upload-progress, si ottengono i byte caricati e totali, che servono a calcolare la percentuale e a mostrarla in tempo reale. Quando arriva upload-success, si aggiorna il testo dell’overlay comunicando che il caricamento è concluso, mentre con upload-response si rimuove del tutto l’overlay. Se avviene un errore (upload-error), si cambia il testo in “Caricamento fallito!” e si fa sparire la finestra dopo un paio di secondi.

Dal lato server, si ascolta soprattutto addAllFinishedListener, che si attiva dopo che tutti i file sono stati processati da Vaadin, indipendentemente dal fatto che abbiano avuto successo o meno. In quell’evento si può per esempio eseguire un piccolo snippet JavaScript che rimuove o nasconde ogni overlay rimasto, nel caso in cui ci siano ancora elementi di interfaccia aperti. Subito dopo, si mostra una Notification di conferma, così l’utente sa che i file sono stati effettivamente ricevuti e che il server ha terminato l’elaborazione. Se uno qualunque dei file fallisce, si riceve addFailedListener, dal quale è possibile aprire una Notification personalizzata di errore.

In questo modo, si ottiene un sistema di caricamento massivo che fornisce un feedback chiaro sia a livello client (con la finestra di caricamento a schermo intero), sia a livello server (con la certezza che l’upload è terminato o che si è verificato un fallimento). L’utente non deve attendere senza indicazioni e non può cliccare altrove per sbaglio, poiché l’overlay copre ogni altro elemento, rendendo più fluida e sicura l’esperienza di caricamento di file anche di grandi dimensioni.

import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.notification.Notification.Position;
import com.vaadin.flow.component.notification.NotificationVariant;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.upload.Upload;
import com.vaadin.flow.component.upload.receivers.MultiFileMemoryBuffer;
import com.vaadin.flow.router.Route;

@Route("")
public class DragDropView extends VerticalLayout {

	private final MultiFileMemoryBuffer buffer = new MultiFileMemoryBuffer();

	public DragDropView() {
		// L'intero layout occupa tutto lo schermo
		setSizeFull();
		// 1. Creo il componente Upload, che però nasconderò
		Upload upload = new Upload(buffer);
		upload.setDropAllowed(false);
		upload.setAutoUpload(true);
		upload.getStyle().set("display", "none"); // Rimane "invisibile" all’utente
		// 2. Creo l’area su cui l’utente farà drop dei file
		VerticalLayout formItem = new VerticalLayout();
		formItem.getStyle().set("border", "2px dashed #999") // Bordo tratteggiato grigio
				.set("border-radius", "8px") // Angoli arrotondati
				.set("padding", "40px") // Spazio interno
				.set("box-sizing", "border-box") // Per sicurezza, calcolo dimensioni
				.set("text-align", "center") // Testo centrato
				.set("transition", "border-color 0.2s"); // Transizione morbida del bordo
		formItem.setSizeFull();
		// 3. Inietto lo script JavaScript che gestisce drag & drop e overlay
		formItem.getElement().executeJs("""
				const dropArea = this;               // Riferimento all'elemento <div> di Vaadin
				const uploadEl = $1;                 // Riferimento al <vaadin-upload> nascosto
				// Funzioni e variabili per l'overlay
				let modalDiv = null;
				let modalContent = null;
				function createModal() {
				  // Crea un div a tutto schermo
				  modalDiv = document.createElement('div');
				  modalDiv.classList.add('uploadProgressDiv'); // se volessimo stile condiviso
				  modalDiv.style.position = 'fixed';
				  modalDiv.style.top = '0';
				  modalDiv.style.left = '0';
				  modalDiv.style.width = '100%';
				  modalDiv.style.height = '100%';
				  modalDiv.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
				  modalDiv.style.display = 'flex';
				  modalDiv.style.justifyContent = 'center';
				  modalDiv.style.alignItems = 'center';
				  modalDiv.style.zIndex = '999999';
				  // Contenitore interno
				  modalContent = document.createElement('div');
				  modalContent.style.backgroundColor = '#fff';
				  modalContent.style.padding = '20px';
				  modalContent.style.borderRadius = '4px';
				  modalContent.style.minWidth = '300px';
				  modalContent.style.minHeight = '80px';
				  modalContent.style.display = 'flex';
				  modalContent.style.flexDirection = 'column';
				  modalContent.style.justifyContent = 'center';
				  modalContent.style.alignItems = 'center';
				  modalContent.innerHTML = 'Inizializzazione...';
				  modalDiv.appendChild(modalContent);
				  document.body.appendChild(modalDiv);
				}
				function updateModal(text) {
				  if (modalContent) {
				    modalContent.innerHTML = text;
				  }
				}
				function removeModal() {
				  if (modalDiv) {
				    document.body.removeChild(modalDiv);
				    modalDiv = null;
				    modalContent = null;
				  }
				}
				// Eventi nativi del web component <vaadin-upload>
				// 1) L'upload inizia
				uploadEl.addEventListener('upload-start', e => {
				  createModal();
				  updateModal('Caricamento iniziato...');
				});
				// 2) Progresso di caricamento
				uploadEl.addEventListener('upload-progress', e => {
				  const loaded = e.detail.file.loaded;
				  const total  = e.detail.file.size;
				  let percent = 0;
				  if (total > 0) {
				    percent = Math.round((loaded / total) * 100);
				  }
				  updateModal('Caricamento: ' + percent + '%');
				});
				// 3) Caricamento concluso con successo
				uploadEl.addEventListener('upload-success', e => {
				  updateModal('Caricamento completato. Attendere la risposta del server...');
				});
				// 4) Caricamento terminato (subito dopo success/error)
				uploadEl.addEventListener('upload-response', e => {
				  removeModal();
				});
				// 5) Se errore
				uploadEl.addEventListener('upload-error', e => {
				  updateModal('Caricamento fallito!');
				  setTimeout(() => removeModal(), 2000);
				});
				// Gestione dell’area di drag & drop personalizzata
				dropArea.addEventListener('dragover', (evt) => {
				  evt.preventDefault();
				  // Aggiungiamo un bordo più "accentuato"
				  dropArea.style.border = '4px dashed var(--lumo-primary-color)';
				  dropArea.style.borderRadius = '8px';
				  dropArea.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.3)';
				});
				dropArea.addEventListener('dragleave', (evt) => {
				  evt.preventDefault();
				  // Torniamo allo stato normale
				  dropArea.style.border = '2px dashed #999';
				  dropArea.style.borderRadius = '8px';
				  dropArea.style.boxShadow = 'none';
				});
				
				dropArea.addEventListener('drop', (evt) => {
				  evt.preventDefault();
				  // Rimuoviamo l'effetto di accentuazione
				  dropArea.style.border = 'none';
				  dropArea.style.boxShadow = 'none';
				  const dtFiles = evt.dataTransfer.files;
				  if (dtFiles.length > 0) {
				    const hiddenInput = uploadEl.$.fileInput;
				    hiddenInput.files = dtFiles;
				    // Forziamo la logica interna
				    if (uploadEl._onFileInputChange) {
				      uploadEl._onFileInputChange({ target: hiddenInput });
				    } else {
				      hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
				    }
				    // Avviamo l’upload
				    if (typeof uploadEl.uploadFiles === 'function') {
				      uploadEl.uploadFiles();
				    }
				  }
				});
				""", formItem.getElement(), upload.getElement());
		// 4. Aggiungo i componenti alla vista
		add(upload, formItem);
		// Eventuali listener lato server
		upload.addAllFinishedListener(event -> {
			UI.getCurrent().getPage().executeJs("const elements = document.getElementsByClassName('uploadProgressDiv');"
					+ "for (let i = 0; i < elements.length; i++) {" + "  elements[i].style.display = 'none';" + "}");
			// Questo scatta quando TUTTI i file caricati sono finiti
			// (success/fail) e Vaadin ha completato le elaborazioni.
			// Possiamo gestire la logica post-upload, ad es. aprire un Dialog
			Notification.show("Caricamento completato lato server!");
		});
		upload.addFailedListener(event -> {
			Notification note = new Notification("Caricamento fallito.");
			note.setPosition(Position.TOP_CENTER);
			note.addThemeVariants(NotificationVariant.LUMO_ERROR);
			note.open();
		});
	}
}

Di luca

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *