ProcessWire Upload Formular mit Drag and Drop

Aus Wikizone
Wechseln zu: Navigation, Suche

Dies ist die Fortsetzung von ProcessWire Upload Formular mit AJAX und damit der dritte Teil der Upload Formular-Serie.

Links

https://www.smashingmagazine.com/2018/01/drag-drop-file-uploader-vanilla-js/ Anleitung für Dieses Tutorial
https://www.ab-heute-programmieren.de/drag-and-drop-upload-mit-html5/ Schönes Beispiel mit einfacherem aber kompatibleren Code.

Dieses mal möchten wir das Upload Formular um eine Drag and Drop Area erweitern.

Drag and Drop Events

Einführung

Es gibt acht Events die der Browser abfeuert:

drag, dragend, dragenter, dragexit, dragleave, dragover, dragstart, drop

Wobei

drag, dragend, dragexit und dragstart

Nur für Seitenelemente wichtig sind die gezogen werden. Wir ziehen Elemente aus dem Dateisystem, daher brauchen wir diese nicht.

Damit man die Events nutzen kann, registriert man ganz normal Handler.

// DROP area 
dropArea = document.getElementById('drop-area');
dropArea.addEventListener('dragenter', handlerFunction, false);
dropArea.addEventListener('dragleave', handlerFunction, false);
dropArea.addEventListener('dragover', handlerFunction, false);
dropArea.addEventListener('drop', handlerFunction, false);

Interessant hier. Wenn man über ein Kindelement der #drop-area zieht wird für die drop-area dragleave gefeuert und für das Kindelement dragenter. Wen man jetzt den Mausbutton losläßt wird trotzdem das dropevent für die drop-area gefeuert, weil sich der drag Event nach oben durchpropagiert.

Damit der Browser nachher auch die Datei hochlädt und nicht im öffnet, muss man auch noch mit event.preventDefault() in jedem Listener das Standardverhalten unterbinden.

Handler für die Drop Area

let dropArea = document.getElementById("drop-area")

// Prevent default drag behaviors
;['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
  dropArea.addEventListener(eventName, preventDefaults, false)   
  document.body.addEventListener(eventName, preventDefaults, false)
})

HTML Drop Area

Wir ergänzen einen Bereich #drop-area in die der User seine Dateien ziehen kann.

Außerdem bauen wir noch ein onchange Event ein. Dazu später mehr.

Das file Input Feld wird über CSS versteckt und das label stlen wir wie einen Button.

<div id="drop-area">
  <input id="fileElem" type="file"  onchange="handleFiles(this.files)" name="images[]" id="images" multiple="multiple" size="40" accept="image/jpg,image/jpeg,image/gif,image/png"/>
  <label for="fileElem" id="images-label" class="upload-button">Select Images</label>
</div>

Anstatt dem change Handler im Feld könnte man auch im JavaScript Code den Handler unterbringen. Da haben wir auch noch etwas mehr Platz für Aufräumarbeiten. Die Browser sind nämlich nicht ganz einheitlich was das Verhalten bei mehrfachem auswählen angeht.

		let fileElem = document.getElementById("fileElem")
		fileElem.addEventListener('change', function (event) {
			handleFiles(this.files)
			// clean up after work due to browser inconsistencies
			fileElem.value = ''
		}, false)

Styling für die Drop Area

#drop-area{
  border: 2px dashed #ddd;
  border-radius: 12px;
  min-width: 200px;
  margin: 50px 0;
  padding: 12px;
  background: #FFF;
}
#drop-area.highlight{
	border-color: purple;
}
.upload-button {
  display: inline-block;
  padding: 10px;
  background: #ccc;
  cursor: pointer;
  border-radius: 5px;
  border: 1px solid #ccc;
}
.upload-button:hover {
  background: #ddd;
}

Drag and Drop Funktionalität

Event Listener

let dropArea = document.getElementById('drop-area') // Referenz zur Drop Area

// Events hinzufügen und default Verhalten des Browsers unterbinden
;['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
  dropArea.addEventListener(eventName, preventDefaults, false)
})
function preventDefaults (e) {
  e.preventDefault()
  e.stopPropagation()
}

// Klassen für das Hovern setzen. Damit können wir dem User visualisieren "Hier bist du richtig"
;['dragenter', 'dragover'].forEach(eventName => { // dragenter UND dragover um bei dragleave der Kindelemente das Entfernen der Klasse zu verhindern
  dropArea.addEventListener(eventName, highlight, false)
})

;['dragleave', 'drop'].forEach(eventName => {
  dropArea.addEventListener(eventName, unhighlight, false)
})

function highlight(e) {
  dropArea.classList.add('highlight')
}

function unhighlight(e) {
  dropArea.classList.remove('highlight')
}

// Jetzt die Drop Funktionalität (User läßt Dateien los)
dropArea.addEventListener('drop', handleDrop, false) // Listener

function handleDrop(e) {
  let dt = e.dataTransfer
  let files = dt.files // unsere Dateien als FileList Objekt (zum besseren Iterieren wird es in handleFiles() in ein Array konvertiert)
  handleFiles(files)
}

function handleFiles(files) {
  ([...files]).forEach(uploadFile) // In Array umwandeln und jedes File uploaden.
}

function uploadFile(file) {
  let url = 'YOUR URL HERE' // Zielskript
  let formData = new FormData() // FormData Objekt

  formData.append('file', file)

  fetch(url, { // Daten senden -> geht nicht mit IE (siehe unten Alternative)
    method: 'POST',
    body: formData
  })
  .then(() => { /* Done. Inform the user */ })
  .catch(() => { /* Error. Inform the user */ })
}

Alternativer Upload mit XML HttpRequest

// XML HttpRequest (vanilla)
function uploadFile(file) {
  var url = 'YOUR URL HERE'
  var xhr = new XMLHttpRequest()
  var formData = new FormData()
  xhr.open('POST', url, true)

  xhr.addEventListener('readystatechange', function(e) {
    if (xhr.readyState == 4 && xhr.status == 200) {
      // Done. Inform the user
    }
    else if (xhr.readyState == 4 && xhr.status != 200) {
      // Error. Inform the user
    }
  })

  formData.append('file', file)
  xhr.send(formData)
}

// Oder mit jQuery

Zusätzliche Funktionalität

Vorschau Bilder

Dazu nutzen wir die FileReader API. Sie arbeitet asnchron, so blockieren wir nicht den Thread

function previewFile(file) {
  let reader = new FileReader() // FileReader Objekt
  reader.readAsDataURL(file)
  reader.onloadend = function() { // Event wenn Bild fertig geladen
    let img = document.createElement('img') // img Element erzeugen
    img.src = reader.result // Src Attribut setzen
    document.getElementById('gallery').appendChild(img) // Bild an die Gallerie hängen
  }
}

Und im HTML an passender Stelle noch den Galeriebereich einsetzen

Und etwas Styling dazu.

#gallery{
	margin-top: 12px;
}
#gallery img{
	width: 150px;
	margin: 5px;
}

Die handleFiles Funktion von oben müssen wir etwas erweitern:

function handleFiles(files) {
  files = [...files]
  files.forEach(uploadFile)
  files.forEach(previewFile)
}

Progress Bar

Hinweis für das alte Beispeil müssen wir zunächst den alten Code für den Upload Indicator entfernen (kompletter Code siehe unten). Auch das HTML Markup ersetzen wir.

HTML5 bietet für solche Zwecke eigens ein progress Tag:

<progress id="progress-bar" max=100 value=0></progress>

Variante 1 - fetch

Bei der Variante mit Fetch (siehe oben) kann man nur Feststellen wann eine Datei komplett hochgeladen ist. Daher können wir auch nur über die Anzahl der Dateien mitteln.

// Vars (gehören an den Anfang des Skripts
let filesDone = 0
let filesToDo = 0
let progressBar = document.getElementById('progress-bar')

//...

function initializeProgress(numfiles) { // reset progress bar
  progressBar.value = 0
  filesDone = 0
  filesToDo = numfiles
}

function progressDone() { // call for each file and calc average then update progressbar
  filesDone++
  progressBar.value = filesDone / filesToDo * 100
}

Jetzt müssen wir wieder zwei alte Funktionen updaten:

function handleFiles(files) {
  files = [...files]
  initializeProgress(files.length) // <- Add this line
  files.forEach(uploadFile)
  files.forEach(previewFile)
}

function uploadFile(file) {
  let url = 'YOUR URL HERE'
  let formData = new FormData()

  formData.append('file', file)

  fetch(url, {
    method: 'POST',
    body: formData
  })
  .then(progressDone) // <- Add `progressDone` call here
  .catch(() => { /* Error. Inform the user */ })
}

Variante 2 - xhr

Mit XHR haben wir auch die Möglichkeit zu schauen wieviel von einer Datei schon hochgeladen ist. Daher bauen wir hier das Ganze etwas aus um bei jeder Datei den Fortschritt anzuzeigen.

// filesDone und filesToDo not needed -> delete
// progressDone not needed -> delete
let uploadProgress = []

//...

function initializeProgress(numFiles) {
  progressBar.value = 0
  uploadProgress = []

  for(let i = numFiles; i > 0; i--) {
    uploadProgress.push(0) // initialize array with an element for each file and set it to 0 (Percent uploaded)
  }
}

function updateProgress(fileNumber, percent) { // check progress of each file and update values in array. Then check average
  uploadProgress[fileNumber] = percent
  let total = uploadProgress.reduce((tot, curr) => tot + curr, 0) / uploadProgress.length
  progressBar.value = total
}

// Change these Funktions on top
function uploadFile(file, i) { // <- Add `i` parameter (Our File Index)
  var url = 'YOUR URL HERE'
  var xhr = new XMLHttpRequest()
  var formData = new FormData()
  xhr.open('POST', url, true)

  // Add following event listener
  xhr.upload.addEventListener("progress", function(e) { // upload eventlistener calls updateProgress Function
    updateProgress(i, (e.loaded * 100.0 / e.total) || 100) 
    // event Object (e) enthält loaded = Anzahl der übermittelten Bytes und total = Gesamtgröße der Datei. 
    //|| 100 ist zur Sicherheit. Bei Fehlern können die Werte 0 sein was einen division by zero Fehler zur Folge hätte.
  })

  xhr.addEventListener('readystatechange', function(e) {
    if (xhr.readyState == 4 && xhr.status == 200) {
      // Done. Inform the user
    }
    else if (xhr.readyState == 4 && xhr.status != 200) {
      // Error. Inform the user
    }
  })

  formData.append('file', file)
  xhr.send(formData)
}

JS

// ************************ Drag and drop ***************** //
let dropArea = document.getElementById("drop-area")

// Prevent default drag behaviors
;['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
  dropArea.addEventListener(eventName, preventDefaults, false)   
  document.body.addEventListener(eventName, preventDefaults, false)
})

// Highlight drop area when item is dragged over it
;['dragenter', 'dragover'].forEach(eventName => {
  dropArea.addEventListener(eventName, highlight, false)
})

;['dragleave', 'drop'].forEach(eventName => {
  dropArea.addEventListener(eventName, unhighlight, false)
})

// Handle dropped files
dropArea.addEventListener('drop', handleDrop, false)

function preventDefaults (e) {
  e.preventDefault()
  e.stopPropagation()
}

function highlight(e) {
  dropArea.classList.add('highlight')
}

function unhighlight(e) {
  dropArea.classList.remove('active')
}

function handleDrop(e) {
  var dt = e.dataTransfer
  var files = dt.files

  handleFiles(files)
}

let uploadProgress = []
let progressBar = document.getElementById('progress-bar')

function initializeProgress(numFiles) {
  progressBar.value = 0
  uploadProgress = []

  for(let i = numFiles; i > 0; i--) {
    uploadProgress.push(0)
  }
}

function updateProgress(fileNumber, percent) {
  uploadProgress[fileNumber] = percent
  let total = uploadProgress.reduce((tot, curr) => tot + curr, 0) / uploadProgress.length
  console.debug('update', fileNumber, percent, total)
  progressBar.value = total
}

function handleFiles(files) {
  files = [...files]
  initializeProgress(files.length)
  files.forEach(uploadFile)
  files.forEach(previewFile)
}

function previewFile(file) {
  let reader = new FileReader()
  reader.readAsDataURL(file)
  reader.onloadend = function() {
    let img = document.createElement('img')
    img.src = reader.result
    document.getElementById('gallery').appendChild(img)
  }
}

function uploadFile(file, i) {
  var url = 'https://api.cloudinary.com/v1_1/joezimim007/image/upload'
  var xhr = new XMLHttpRequest()
  var formData = new FormData()
  xhr.open('POST', url, true)
  xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest')

  // Update progress (can be used to show progress indicator)
  xhr.upload.addEventListener("progress", function(e) {
    updateProgress(i, (e.loaded * 100.0 / e.total) || 100)
  })

  xhr.addEventListener('readystatechange', function(e) {
    if (xhr.readyState == 4 && xhr.status == 200) {
      updateProgress(i, 100) // <- Add this
    }
    else if (xhr.readyState == 4 && xhr.status != 200) {
      // Error. Inform the user
    }
  })

  formData.append('upload_preset', 'ujpu6gyk')
  formData.append('file', file)
  xhr.send(formData)
}

CSS

/* Drop Area */
#drop-area{
	border: 2px dashed #ddd;
	border-radius: 12px;
	min-width: 200px;
	margin: 50px 0;
	padding: 12px;
	background: #FFF;
}
#drop-area.highlight{
	border-color: purple;
}
#gallery{
	margin-top: 12px;
}
#gallery img{
	width: 150px;
	margin: 5px;
}