Drag and Drop Datei-Upload

Avatar of Osvaldas Valutis
Osvaldas Valutis am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Ich arbeite an einer RSS-Reader-App namens Readerrr (Anmerkung des Herausgebers: Link entfernt, da die Seite tot zu sein scheint). Ich wollte die Feed-Import-Erfahrung verbessern, indem ich neben dem traditionellen Dateieingabefeld auch das Hochladen von Dateien per Drag & Drop ermöglichte. Manchmal ist Drag & Drop eine bequemere Methode zur Dateiauswahl, nicht wahr?

Demo ansehen

Markup

Dieses Markup hat nichts Spezielles mit Drag & Drop zu tun. Es ist nur ein normales, funktionales <form>, wenn auch mit einigen zusätzlichen HTML-Elementen für mögliche Zustände.

<form class="box" method="post" action="" enctype="multipart/form-data">
  <div class="box__input">
    <input class="box__file" type="file" name="files[]" id="file" data-multiple-caption="{count} files selected" multiple />
    <label for="file"><strong>Choose a file</strong><span class="box__dragndrop"> or drag it here</span>.</label>
    <button class="box__button" type="submit">Upload</button>
  </div>
  <div class="box__uploading">Uploading…</div>
  <div class="box__success">Done!</div>
  <div class="box__error">Error! <span></span>.</div>
</form>

Diese Zustände verstecken wir, bis wir sie brauchen

.box__dragndrop,
.box__uploading,
.box__success,
.box__error {
  display: none;
}

Eine kleine Erklärung

  • Bezüglich der Zustände: Das Element .box__uploading wird während des Ajax-Upload-Prozesses sichtbar sein (und die anderen bleiben noch verborgen). Dann wird .box__success oder .box__error je nachdem, was passiert, angezeigt.
  • input[type="file"] und label sind die funktionalen Teile des Formulars. Ich habe in meinem Beitrag über die Anpassung von Dateieingabefeldern geschrieben, wie man diese zusammen gestaltet. In diesem Beitrag habe ich auch den Zweck des Attributs [data-multiple-caption] beschrieben. Die Eingabe und das Label dienen auch als Alternative zur Auswahl von Dateien auf die Standardmethode (oder als einzige Methode, wenn Drag & Drop nicht unterstützt wird).
  • .box__dragndrop wird angezeigt, wenn ein Browser die Drag & Drop-Funktionalität zum Hochladen von Dateien unterstützt.

Funktionserkennung

Wir können uns nicht zu 100 % auf die Unterstützung von Drag & Drop durch Browser verlassen. Wir sollten eine Fallback-Lösung bereitstellen. Und so: Funktionserkennung. Drag & Drop-Dateiupload hängt von einer Reihe verschiedener JavaScript-APIs ab, daher müssen wir alle davon prüfen.

Zuerst die Drag & Drop-Ereignisse selbst. Modernizr ist eine Bibliothek, der Sie in Bezug auf Funktionserkennung vertrauen können. Dieser Test stammt von dort.

var div = document.createElement('div');
return ('draggable' in div) || ('ondragstart' in div && 'ondrop' in div)

Als Nächstes müssen wir die FormData-Schnittstelle prüfen, die zum Erstellen eines programmatischen Objekts der ausgewählten Datei(en) dient, damit diese über Ajax an den Server gesendet werden können.

return 'FormData' in window;

Zuletzt benötigen wir das DataTransfer-Objekt. Dieses ist etwas knifflig, da es keine narrensichere Methode gibt, die Verfügbarkeit des Objekts vor der ersten Interaktion des Benutzers mit der Drag & Drop-Schnittstelle zu erkennen. Nicht alle Browser stellen das Objekt bereit.

Idealerweise möchten wir eine UX vermeiden wie…

  • „Ziehen Sie Dateien hierher!“
  • [Benutzer zieht Dateien und lässt sie fallen]
  • „Ups, nur ein Scherz, Drag & Drop wird nicht unterstützt.“

Der Trick hierbei ist, die Verfügbarkeit der FileReader API genau dann zu prüfen, wenn das Dokument geladen wird. Die Idee dahinter ist, dass Browser, die FileReader unterstützen, auch DataTransfer unterstützen.

'FileReader' in window

Wenn wir den obigen Code in eine selbstanrufende anonyme Funktion kombinieren…

var isAdvancedUpload = function() {
  var div = document.createElement('div');
  return (('draggable' in div) || ('ondragstart' in div && 'ondrop' in div)) && 'FormData' in window && 'FileReader' in window;
}();

…können wir eine effektive Erkennung der Funktionsunterstützung durchführen.

if (isAdvancedUpload) {
  // ...
}

Mit dieser funktionierenden Funktionserkennung können wir den Benutzern nun mitteilen, dass sie ihre Dateien per Drag & Drop in unser Formular ziehen können (oder auch nicht). Wir können das Formular gestalten, indem wir ihm eine Klasse hinzufügen, falls die Unterstützung vorhanden ist.

var $form = $('.box');

if (isAdvancedUpload) {
  $form.addClass('has-advanced-upload');
}
.box.has-advanced-upload {
  background-color: white;
  outline: 2px dashed black;
  outline-offset: -10px;
}
.box.has-advanced-upload .box__dragndrop {
  display: inline;
}

Kein Problem, wenn der Drag & Drop-Dateiupload nicht unterstützt wird. Benutzer können Dateien über das gute alte input[type="file"] hochladen!

Hinweis zur Browserunterstützung: Microsoft Edge hat einen Fehler, der Drag & Drop verhindert. Es scheint, als seien sie sich dessen bewusst und hoffen, ihn zu beheben. (Update: Link zum Fehler entfernt, da der Link nicht mehr funktionierte. Da Edge jetzt Chromium ist, ist das wahrscheinlich kein Problem mehr.)

Drag & Drop

Los geht's, hier ist der gute Stoff.

Dieser Teil befasst sich mit dem Hinzufügen und Entfernen von Klassen zum Formular in den verschiedenen Zuständen, z. B. wenn der Benutzer eine Datei über das Formular zieht. Dann werden diese Dateien aufgefangen, wenn sie fallen gelassen werden.

if (isAdvancedUpload) {

  var droppedFiles = false;

  $form.on('drag dragstart dragend dragover dragenter dragleave drop', function(e) {
    e.preventDefault();
    e.stopPropagation();
  })
  .on('dragover dragenter', function() {
    $form.addClass('is-dragover');
  })
  .on('dragleave dragend drop', function() {
    $form.removeClass('is-dragover');
  })
  .on('drop', function(e) {
    droppedFiles = e.originalEvent.dataTransfer.files;
  });

}
  • e.preventDefault() und e.stopPropagation() verhindern unerwünschte Verhaltensweisen für die zugewiesenen Ereignisse in verschiedenen Browsern.
  • e.originalEvent.dataTransfer.files gibt die Liste der fallen gelassenen Dateien zurück. Gleich werden Sie sehen, wie diese Daten zum Senden der Dateien an den Server verwendet werden.

Durch das Hinzufügen und Entfernen von .is-dragover, wenn nötig, können wir visuell anzeigen, wann es für einen Benutzer sicher ist, die Dateien fallen zu lassen.

.box.is-dragover {
  background-color: grey;
}

Dateien auf traditionelle Weise auswählen

Manchmal ist das Ziehen und Ablegen von Dateien keine sehr komfortable Methode zur Auswahl von Dateien für den Upload. Besonders wenn ein Benutzer vor einem Computer mit kleinem Bildschirm sitzt. Daher wäre es schön, den Benutzern die Wahl der bevorzugten Methode zu überlassen. Das Dateieingabefeld und das Label sind hier, um dies zu ermöglichen. Sie beide so zu gestalten, wie ich es beschrieben habe, ermöglicht es uns, die Benutzeroberfläche konsistent zu halten.

Ajax-Upload

Es gibt keine Cross-Browser-Möglichkeit, per Drag & Drop hochgeladene Dateien ohne Ajax hochzuladen. Einige Browser (IE und Firefox) erlauben nicht das Setzen des Werts eines Dateieingabefeldes, das dann auf übliche Weise an den Server übermittelt werden könnte.

Das **funktioniert nicht**

$form.find('input[type="file"]').prop('files', droppedFiles);

Stattdessen verwenden wir Ajax, wenn das Formular abgeschickt wird.

$form.on('submit', function(e) {
  if ($form.hasClass('is-uploading')) return false;

  $form.addClass('is-uploading').removeClass('is-error');

  if (isAdvancedUpload) {
    // ajax for modern browsers
  } else {
    // ajax for legacy browsers
  }
});

Die Klasse .is-uploading erfüllt eine doppelte Funktion: Sie verhindert wiederholtes Absenden des Formulars (return false) und hilft dem Benutzer anzuzeigen, dass die Übermittlung gerade stattfindet.

.box.is-uploading .box__input {
  visibility: none;
}
.box.is-uploading .box__uploading {
  display: block;
}

Ajax für moderne Browser

Wenn dies ein Formular ohne Dateiupload wäre, bräuchten wir keine zwei verschiedenen Ajax-Techniken. Leider wird das Hochladen von Dateien über XMLHttpRequest in IE 9 und darunter nicht unterstützt.

Um zu unterscheiden, welche Ajax-Methode funktioniert, können wir unseren vorhandenen isAdvancedUpload-Test verwenden, denn die Browser, die die von mir erwähnten Funktionen unterstützen, unterstützen auch den Datei-Upload über XMLHttpRequest. Hier ist ein Code, der unter IE 10+ funktioniert.

if (isAdvancedUpload) {
  e.preventDefault();

  var ajaxData = new FormData($form.get(0));

  if (droppedFiles) {
    $.each( droppedFiles, function(i, file) {
      ajaxData.append( $input.attr('name'), file );
    });
  }

  $.ajax({
    url: $form.attr('action'),
    type: $form.attr('method'),
    data: ajaxData,
    dataType: 'json',
    cache: false,
    contentType: false,
    processData: false,
    complete: function() {
      $form.removeClass('is-uploading');
    },
    success: function(data) {
      $form.addClass( data.success == true ? 'is-success' : 'is-error' );
      if (!data.success) $errorMsg.text(data.error);
    },
    error: function() {
      // Log the error, show an alert, whatever works for you
    }
  });
}
  • FormData($form.get(0)) sammelt Daten von allen Formularfeldern.
  • Die $.each()-Schleife durchläuft die per Drag & Drop abgelegten Dateien. ajaxData.append() fügt sie dem Datenstapel hinzu, der per Ajax übermittelt wird.
  • data.success und data.error sind eine JSON-formatierte Antwort, die vom Server zurückgegeben wird. So würde das in PHP aussehen.
<?php
  // ...
  die(json_encode([ 'success'=> $is_success, 'error'=> $error_msg]));
?>

Ajax für Legacy-Browser

Dies ist im Wesentlichen für IE 9-. Wir müssen keine per Drag & Drop abgelegten Dateien sammeln, denn in diesem Fall (isAdvancedUpload = false) unterstützt der Browser keinen Drag & Drop-Dateiupload und das Formular verlässt sich nur auf das input[type="file"].

Seltsamerweise funktioniert das Anvisieren des Formulars auf einem dynamisch eingefügten iframe.

if (isAdvancedUpload) {
  // ...
} else {
  var iframeName  = 'uploadiframe' + new Date().getTime();
    $iframe   = $('<iframe name="' + iframeName + '" style="display: none;"></iframe>');

  $('body').append($iframe);
  $form.attr('target', iframeName);

  $iframe.one('load', function() {
    var data = JSON.parse($iframe.contents().find('body' ).text());
    $form
      .removeClass('is-uploading')
      .addClass(data.success == true ? 'is-success' : 'is-error')
      .removeAttr('target');
    if (!data.success) $errorMsg.text(data.error);
    $form.removeAttr('target');
    $iframe.remove();
  });
}

Automatische Übermittlung

Wenn Sie ein einfaches Formular mit nur einem Drag & Drop-Bereich oder einem Dateieingabefeld haben, kann es für den Benutzer praktisch sein, wenn er nicht die Schaltfläche drücken muss. Stattdessen können Sie das Formular automatisch absenden, indem Sie das submit-Ereignis auslösen, sobald eine Datei fallen gelassen oder ausgewählt wurde.

// ...

.on('drop', function(e) { // when drag & drop is supported
  droppedFiles = e.originalEvent.dataTransfer.files;
  $form.trigger('submit');
});

// ...

$input.on('change', function(e) { // when drag & drop is NOT supported
  $form.trigger('submit');
});

Wenn der Drag & Drop-Bereich visuell gut gestaltet ist (es ist für den Benutzer offensichtlich, was zu tun ist), könnten Sie in Erwägung ziehen, die Absendeschaltfläche zu verstecken (weniger UI kann gut sein). Aber seien Sie vorsichtig, wenn Sie eine solche Steuerung ausblenden. Die Schaltfläche sollte sichtbar und funktionsfähig sein, falls JavaScript aus irgendeinem Grund nicht verfügbar ist (progressive enhancement!). Das Hinzufügen eines .no-js-Klassennamens zu und dessen Entfernung mit JavaScript erledigt die Aufgabe.

<html class="no-js">
  <head>
    <!-- remove this if you use Modernizr -->
    <script>(function(e,t,n){var r=e.querySelectorAll("html")[0];r.className=r.className.replace(/(^|\s)no-js(\s|$)/,"$1js$2")})(document,window,0);</script>
  </head>
</html>
.box__button {
  display: none;
}
.no-js .box__button {
  display: block;
}

Ausgewählte Dateien anzeigen

Wenn Sie keine automatische Übermittlung durchführen, sollte dem Benutzer angezeigt werden, ob er erfolgreich Dateien ausgewählt hat.

var $input    = $form.find('input[type="file"]'),
    $label    = $form.find('label'),
    showFiles = function(files) {
      $label.text(files.length > 1 ? ($input.attr('data-multiple-caption') || '').replace( '{count}', files.length ) : files[ 0 ].name);
    };

// ...

.on('drop', function(e) {
  droppedFiles = e.originalEvent.dataTransfer.files; // the files that were dropped
  showFiles( droppedFiles );
});

//...

$input.on('change', function(e) {
  showFiles(e.target.files);
});

Wenn JavaScript nicht verfügbar ist

Progressive Enhancement ist die Idee, dass ein Benutzer die wichtigsten Aufgaben auf einer Website unabhängig von allem abschließen können sollte. Dateiupload ist keine Ausnahme. Wenn aus irgendeinem Grund JavaScript nicht verfügbar ist, sieht die Benutzeroberfläche so aus:

Die Seite wird beim Absenden des Formulars neu geladen. Unser JavaScript zur Anzeige des Übermittlungsergebnisses ist nutzlos. Das bedeutet, wir müssen uns auf serverseitige Lösungen verlassen. So sieht es auf der Demo-Seite aus und funktioniert dort.

<?php

  $upload_success = null;
  $upload_error = '';

  if (!empty($_FILES['files'])) {
    /*
      the code for file upload;
      $upload_success – becomes "true" or "false" if upload was unsuccessful;
      $upload_error – an error message of if upload was unsuccessful;
    */
  }

?>

Und einige Anpassungen für das Markup.

<form class="box" method="post" action="" enctype="multipart/form-data">

  <?php if ($upload_success === null): ?>

  <div class="box__input">
    <!-- ... -->
  </div>

  <?php endif; ?>

  <!-- ... -->

  <div class="box__success"<?php if( $upload_success === true ): ?> style="display: block;"<?php endif; ?>>Done!</div>
  <div class="box__error"<?php if( $upload_success === false ): ?> style="display: block;"<?php endif; ?>>Error! <span><?=$upload_error?></span>.</div>

</form>

Das war's! Dieser bereits lange Artikel könnte noch länger sein, aber ich denke, dies wird Ihnen den Einstieg in ein verantwortungsvolles Drag & Drop-Dateiupload-Feature für Ihre eigenen Projekte erleichtern.

Schauen Sie sich die Demo für weitere Details an (zeigen Sie die Quellcode an, um das JavaScript ohne jQuery-Abhängigkeit zu sehen).

Demo ansehen