ProcessWire Upload Formular mit Drag and Drop: Unterschied zwischen den Versionen
| (11 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt) | |||
| Zeile 39: | Zeile 39: | ||
}) | }) | ||
</syntaxhighlight> | </syntaxhighlight> | ||
| + | |||
| + | == Praktische Umsetzung == | ||
| + | In der Praxis gehen wir wie unten beschrieben vor. Das Beispiel basiert auf dem Tutorial (s.o.) Zusätzlich müssen wir noch folgendes beachen: Da die Bilder schon vor den Formulardaten hochgeladen werden, brauchen wir eine Lösung um die Daten später zusammenzufügen. Bei einer vorherigen Registrierung wäre das einfach. Da man immer weiß wer gerade am Formular arbeitet. Wir könnten aber ein verstecktes Feld einsetzen, in dem wir die Uploads verwalten und diese dann später aus dem temporären Upload Ordner auslesen. Schlussendlich sollten wir auch noch regelmäßig den Upload Ordner säubern. Zur Sicherheit sollten keine ausführbaren Dateien in diesem Ordner zugelassen sein. | ||
== HTML Drop Area == | == HTML Drop Area == | ||
| − | Wir ergänzen einen Bereich #drop-area in die der User seine Dateien ziehen kann. | + | Das alte Images Uploadfeld brauchen wir nicht mehr. Man könnte es als Fallback nutzen. Aber beim Upload von mehreren Bildern muss man viele Nachteile in Kauf nehmen und kommt auch evtl. in Timeout und Max-Size Probleme. Daher schmeißen wir das ganz weg und ersetzen es. |
| + | |||
| + | Wir ergänzen einen Bereich #drop-area in die der User seine Dateien ziehen kann. Dazu noch einen Bereich für die Vorschau-Bilder und einen Fortschrittsbalken. | ||
Außerdem bauen wir noch ein onchange Event ein. Dazu später mehr. | Außerdem bauen wir noch ein onchange Event ein. Dazu später mehr. | ||
| − | Das file Input Feld wird über CSS versteckt und das label | + | Das file Input Feld wird über CSS versteckt und das label stylen wir wie einen Button. |
| + | |||
<syntaxhighlight lang="html5"> | <syntaxhighlight lang="html5"> | ||
<div id="drop-area"> | <div id="drop-area"> | ||
| − | <input id="fileElem" type="file | + | <input id="fileElem" type="file" 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> | <label for="fileElem" id="images-label" class="upload-button">Select Images</label> | ||
</div> | </div> | ||
| + | <progress id="progress-bar" max=100 value=0></progress> | ||
| + | <div id="gallery"></div> | ||
| + | <div id="uploaded-messages" class="messages"><?php if(isset($errors['uploaded'])) echo showError($errors['uploaded']); ?></div> | ||
| + | |||
</syntaxhighlight> | </syntaxhighlight> | ||
| + | |||
| + | 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. | ||
| + | <syntaxhighlight lang="javascript"> | ||
| + | let fileElem = document.getElementById("fileElem") | ||
| + | fileElem.addEventListener('change', function (event) { | ||
| + | handleFiles(this.files) | ||
| + | // clean up after work due to browser inconsistencies | ||
| + | fileElem.value = '' | ||
| + | }, false) | ||
| + | </syntaxhighlight> | ||
| + | |||
=== Styling für die Drop Area === | === Styling für die Drop Area === | ||
<syntaxhighlight lang="css"> | <syntaxhighlight lang="css"> | ||
| Zeile 77: | Zeile 98: | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
| + | |||
== Drag and Drop Funktionalität == | == Drag and Drop Funktionalität == | ||
=== Event Listener === | === Event Listener === | ||
| Zeile 162: | Zeile 184: | ||
== Zusätzliche Funktionalität == | == Zusätzliche Funktionalität == | ||
| + | |||
=== Vorschau Bilder === | === Vorschau Bilder === | ||
Dazu nutzen wir die FileReader API. Sie arbeitet asnchron, so blockieren wir nicht den Thread | Dazu nutzen wir die FileReader API. Sie arbeitet asnchron, so blockieren wir nicht den Thread | ||
| Zeile 197: | Zeile 220: | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
| + | |||
=== Progress Bar === | === 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: | HTML5 bietet für solche Zwecke eigens ein progress Tag: | ||
<progress id="progress-bar" max=100 value=0></progress> | <progress id="progress-bar" max=100 value=0></progress> | ||
| + | |||
==== Variante 1 - fetch ==== | ==== 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. | 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. | ||
| Zeile 298: | Zeile 325: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
| − | === JS === | + | |
| + | == Upload und weitere Formularfelder zusammenführen == | ||
| + | Da die Bilder schon vor den restlichen Formulardaten auf dem Server landen, müsssen wir irgendwie dafür sorgen, dass am Ende die bereits hochgeladenen Bilder und die Formulardaten zusammengefügt werden. Dazu sind mehrere Möglichkeiten denkbar. In unserem Fall nutzen wir einfach ein verstecktes Formularfeld, in dem wir die hochgeladenen Bilder mit dem vom Server generierten Namen hochladen. | ||
| + | === Bereits hochgeladene Dateien tracken === | ||
| + | <input type="hidden" name="uploaded[]" id="uploaded" value=""/> | ||
| + | In diesem Feld wollen wir die Bildnamen speichern. | ||
| + | Nicht mehr benötigt ist das required für das Images Feld. Das nutzen wir jetzt nicht mehr direkt. Die Bilder werden ja separat über AJAX hochgeladen und nicht zusammen mit den restlichen Formulardaten. | ||
| + | |||
| + | Damit das PHP Skript erkennt, wenn ein Upload vorliegt übermitteln wir zusätzlich eine Variable upload_preset. Das ist von Cloudinary abgeschaut ;-) | ||
| + | formData.append('upload_preset', 'one') | ||
| + | So können wir theoretisch sverschiedene Verarbeitungsmethoden ansteuern. | ||
| + | |||
| + | === WireUpload in ProcessWire === | ||
| + | * Neue Funktion | ||
| + | * Alte WireUpload Funktion abschalten | ||
| + | * Fehler auslesen | ||
| + | * Fehlerrückgabe, Messsages ausgeben | ||
| + | * File Resizing | ||
| + | * evtl. Thumb löschen oder markieren | ||
| + | |||
| + | === Upload Handeln === | ||
| + | Wir nutzen ein verstecktes Feld um die bereits hochgeladenen Dateien zu speichern. Da JSON Gänsefüßchen enthält müssen wir JSON noch zusätzlich URL Enkodieren. | ||
| + | In PHP erweitern wir den Sanitizer um den Typ hidden | ||
| + | |||
| + | <input type="hidden" name="uploaded" id="uploaded" value=""/> | ||
| + | |||
| + | <pre> | ||
| + | ... | ||
| + | if($f['type'] == 'text' || $f['type'] == 'hidden'){ | ||
| + | $form_fields[$key]['value'] = $sanitizer->text($input->post->$key); | ||
| + | } | ||
| + | </pre> | ||
| + | |||
| + | Und fügen im Array $form_fields das Feld hinzu | ||
| + | 'uploaded' => array('type' => 'hidden', 'value' => 0, 'required' => true)// handled separately | ||
| + | |||
| + | In form-process.php fügen wir noch eine weitere Variable ein | ||
| + | $uploadResponse = array( 'success' => false, 'errors' => null, 'messages' => array(), 'uploaded' => '', ); | ||
| + | Außerdem fällt einiges weg, da wir keine normale Übertragung mehr brauchen. Die App wird komplett auf AJAX laufen. | ||
| + | |||
| + | Zusätzlich brauchen wir den PHP Code für die Upload Verarbeitung. Diese läuft jetzt getrennt von der Verarbeitung des restlichen Formulars. Die Bilder werden ja immer sofort hochgespielt. Damit wir später noch wissen welche es waren und welchen Namen auf dem Server sie bekommen haben nutzen wir das versteckte Feld. Der PHP Code schickt die Namen an das Skript zurück. | ||
| + | |||
| + | <syntaxhighlight lang="php"> | ||
| + | /** | ||
| + | * AJAX UPLOAD submitted NEW | ||
| + | */ | ||
| + | if($ajax && $input->post->upload_preset){ | ||
| + | // RC: create temp path if it isn't there already | ||
| + | if(!is_dir($upload_path)) { | ||
| + | if(!wireMkdir($upload_path)) throw new WireException("No upload path!"); | ||
| + | } | ||
| + | // cleanup | ||
| + | deleteOlderFiles($upload_path,1200); | ||
| + | // setup new wire upload | ||
| + | $u = new WireUpload('images'); | ||
| + | //... | ||
| + | |||
| + | // start the upload of the files | ||
| + | $files = $u->execute(); | ||
| + | if( count( $u->getErrors() ) ){ | ||
| + | $errors = $u->getErrors(); | ||
| + | $ajaxResponse['success'] = false; | ||
| + | $ajaxResponse['errors'] = $errors; | ||
| + | echo(json_encode($ajaxResponse)); | ||
| + | }else{ // successfull uploaded | ||
| + | foreach($files as $filename) { | ||
| + | $ajaxResponse['messages'][] = $filename.' - Upload erfolgreich'; | ||
| + | $ajaxResponse['uploaded'][] = $filename; | ||
| + | } | ||
| + | $ajaxResponse['success'] = true; | ||
| + | echo(json_encode($ajaxResponse)); | ||
| + | } | ||
| + | } | ||
| + | |||
| + | </syntaxhighlight> | ||
| + | Dafür fällt der Upload Teil im Processing weg (siehe unten im Codeteil). Und zwar der ganze Zweig ab: | ||
| + | // if no errors when uploading files | ||
| + | if(!$u->getErrors()){... | ||
| + | Da wir vorher schon den Upload versorgt haben können wir jetzt anders vorgehen. Wenn alles im Formular stimmt kümmern wir uns direkt um das Erstellen der Seite. | ||
| + | |||
| + | (siehe Code unten) | ||
| + | |||
| + | Außerdem bauen wir noch eine kleine Funktion zum Löschen alter Dateien ein. Aufgerufen wird Sie jedesmal wenn neue Bilder hochgespielt werden (siehe oben) | ||
| + | <syntaxhighlight lang="php"> | ||
| + | function deleteOlderFiles( $dir='temp/', $seconds=86400, $fileExtension='' ){ | ||
| + | foreach (glob($dir."*".$fileExtension) as $file) { | ||
| + | //For jpg images this would be: glob($dir."*.jpg") | ||
| + | if(time() - filectime($file) > $seconds){ | ||
| + | unlink($file); | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | ==== JavaScript für den Upload ==== | ||
| + | |||
| + | == Aufräumarbeiten == | ||
| + | Beim Anlegen der ProcessWire Seite löschen wir eigentlich bereits die Datein aus dem temporären Bilderordner. Aber es könnte vielleicht sein, dass durch Skriptabbrüche etc. vielleicht mal noch ein paar Bilder rumliegen. Daher löschen wir über eine Funktion auch alle älteren Bilder. | ||
| + | |||
| + | == File Resize == | ||
| + | Eine weitere praktische Zusatzfunktion wäre die Möglichkeit schon im Browser die Bilder zu verkleinern. So sparen wir Zeit beim Upload. Das ist nicht ganz trivial vor allem wenn wir ältere Geräte berücksichtigen wollen. Beachten muss man, dass man immer auf das Ergebnis warten muss bevor man den Upload startet. | ||
| + | |||
| + | https://www.askingbox.com/tutorial/how-to-resize-image-before-upload-in-browser | ||
| + | https://github.com/nodeca/pica | ||
| + | |||
| + | == Code Zusammengefaßt == | ||
| + | === JS Basis für Drag and Drop === | ||
<syntaxhighlight lang="javascript"> | <syntaxhighlight lang="javascript"> | ||
// ************************ Drag and drop ***************** // | // ************************ Drag and drop ***************** // | ||
| Zeile 426: | Zeile 559: | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
| + | |||
| + | == Tricks und Hinweise == | ||
| + | === On Load ohne jQuery === | ||
| + | [[JavaScript - onload Event]] | ||
Aktuelle Version vom 19. Februar 2019, 11:54 Uhr
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[Bearbeiten]
Einführung[Bearbeiten]
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[Bearbeiten]
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)
})
Praktische Umsetzung[Bearbeiten]
In der Praxis gehen wir wie unten beschrieben vor. Das Beispiel basiert auf dem Tutorial (s.o.) Zusätzlich müssen wir noch folgendes beachen: Da die Bilder schon vor den Formulardaten hochgeladen werden, brauchen wir eine Lösung um die Daten später zusammenzufügen. Bei einer vorherigen Registrierung wäre das einfach. Da man immer weiß wer gerade am Formular arbeitet. Wir könnten aber ein verstecktes Feld einsetzen, in dem wir die Uploads verwalten und diese dann später aus dem temporären Upload Ordner auslesen. Schlussendlich sollten wir auch noch regelmäßig den Upload Ordner säubern. Zur Sicherheit sollten keine ausführbaren Dateien in diesem Ordner zugelassen sein.
HTML Drop Area[Bearbeiten]
Das alte Images Uploadfeld brauchen wir nicht mehr. Man könnte es als Fallback nutzen. Aber beim Upload von mehreren Bildern muss man viele Nachteile in Kauf nehmen und kommt auch evtl. in Timeout und Max-Size Probleme. Daher schmeißen wir das ganz weg und ersetzen es.
Wir ergänzen einen Bereich #drop-area in die der User seine Dateien ziehen kann. Dazu noch einen Bereich für die Vorschau-Bilder und einen Fortschrittsbalken.
Außerdem bauen wir noch ein onchange Event ein. Dazu später mehr.
Das file Input Feld wird über CSS versteckt und das label stylen wir wie einen Button.
<div id="drop-area">
<input id="fileElem" type="file" 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>
<progress id="progress-bar" max=100 value=0></progress>
<div id="gallery"></div>
<div id="uploaded-messages" class="messages"><?php if(isset($errors['uploaded'])) echo showError($errors['uploaded']); ?></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[Bearbeiten]
#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[Bearbeiten]
Event Listener[Bearbeiten]
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[Bearbeiten]
// 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[Bearbeiten]
Vorschau Bilder[Bearbeiten]
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[Bearbeiten]
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[Bearbeiten]
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[Bearbeiten]
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)
}
Upload und weitere Formularfelder zusammenführen[Bearbeiten]
Da die Bilder schon vor den restlichen Formulardaten auf dem Server landen, müsssen wir irgendwie dafür sorgen, dass am Ende die bereits hochgeladenen Bilder und die Formulardaten zusammengefügt werden. Dazu sind mehrere Möglichkeiten denkbar. In unserem Fall nutzen wir einfach ein verstecktes Formularfeld, in dem wir die hochgeladenen Bilder mit dem vom Server generierten Namen hochladen.
Bereits hochgeladene Dateien tracken[Bearbeiten]
<input type="hidden" name="uploaded[]" id="uploaded" value=""/>
In diesem Feld wollen wir die Bildnamen speichern. Nicht mehr benötigt ist das required für das Images Feld. Das nutzen wir jetzt nicht mehr direkt. Die Bilder werden ja separat über AJAX hochgeladen und nicht zusammen mit den restlichen Formulardaten.
Damit das PHP Skript erkennt, wenn ein Upload vorliegt übermitteln wir zusätzlich eine Variable upload_preset. Das ist von Cloudinary abgeschaut ;-)
formData.append('upload_preset', 'one')
So können wir theoretisch sverschiedene Verarbeitungsmethoden ansteuern.
WireUpload in ProcessWire[Bearbeiten]
- Neue Funktion
- Alte WireUpload Funktion abschalten
- Fehler auslesen
- Fehlerrückgabe, Messsages ausgeben
- File Resizing
- evtl. Thumb löschen oder markieren
Upload Handeln[Bearbeiten]
Wir nutzen ein verstecktes Feld um die bereits hochgeladenen Dateien zu speichern. Da JSON Gänsefüßchen enthält müssen wir JSON noch zusätzlich URL Enkodieren. In PHP erweitern wir den Sanitizer um den Typ hidden
<input type="hidden" name="uploaded" id="uploaded" value=""/>
...
if($f['type'] == 'text' || $f['type'] == 'hidden'){
$form_fields[$key]['value'] = $sanitizer->text($input->post->$key);
}
Und fügen im Array $form_fields das Feld hinzu
'uploaded' => array('type' => 'hidden', 'value' => 0, 'required' => true)// handled separately
In form-process.php fügen wir noch eine weitere Variable ein
$uploadResponse = array( 'success' => false, 'errors' => null, 'messages' => array(), 'uploaded' => , );
Außerdem fällt einiges weg, da wir keine normale Übertragung mehr brauchen. Die App wird komplett auf AJAX laufen.
Zusätzlich brauchen wir den PHP Code für die Upload Verarbeitung. Diese läuft jetzt getrennt von der Verarbeitung des restlichen Formulars. Die Bilder werden ja immer sofort hochgespielt. Damit wir später noch wissen welche es waren und welchen Namen auf dem Server sie bekommen haben nutzen wir das versteckte Feld. Der PHP Code schickt die Namen an das Skript zurück.
/**
* AJAX UPLOAD submitted NEW
*/
if($ajax && $input->post->upload_preset){
// RC: create temp path if it isn't there already
if(!is_dir($upload_path)) {
if(!wireMkdir($upload_path)) throw new WireException("No upload path!");
}
// cleanup
deleteOlderFiles($upload_path,1200);
// setup new wire upload
$u = new WireUpload('images');
//...
// start the upload of the files
$files = $u->execute();
if( count( $u->getErrors() ) ){
$errors = $u->getErrors();
$ajaxResponse['success'] = false;
$ajaxResponse['errors'] = $errors;
echo(json_encode($ajaxResponse));
}else{ // successfull uploaded
foreach($files as $filename) {
$ajaxResponse['messages'][] = $filename.' - Upload erfolgreich';
$ajaxResponse['uploaded'][] = $filename;
}
$ajaxResponse['success'] = true;
echo(json_encode($ajaxResponse));
}
}
Dafür fällt der Upload Teil im Processing weg (siehe unten im Codeteil). Und zwar der ganze Zweig ab:
// if no errors when uploading files
if(!$u->getErrors()){...
Da wir vorher schon den Upload versorgt haben können wir jetzt anders vorgehen. Wenn alles im Formular stimmt kümmern wir uns direkt um das Erstellen der Seite.
(siehe Code unten)
Außerdem bauen wir noch eine kleine Funktion zum Löschen alter Dateien ein. Aufgerufen wird Sie jedesmal wenn neue Bilder hochgespielt werden (siehe oben)
function deleteOlderFiles( $dir='temp/', $seconds=86400, $fileExtension='' ){
foreach (glob($dir."*".$fileExtension) as $file) {
//For jpg images this would be: glob($dir."*.jpg")
if(time() - filectime($file) > $seconds){
unlink($file);
}
}
}
JavaScript für den Upload[Bearbeiten]
Aufräumarbeiten[Bearbeiten]
Beim Anlegen der ProcessWire Seite löschen wir eigentlich bereits die Datein aus dem temporären Bilderordner. Aber es könnte vielleicht sein, dass durch Skriptabbrüche etc. vielleicht mal noch ein paar Bilder rumliegen. Daher löschen wir über eine Funktion auch alle älteren Bilder.
File Resize[Bearbeiten]
Eine weitere praktische Zusatzfunktion wäre die Möglichkeit schon im Browser die Bilder zu verkleinern. So sparen wir Zeit beim Upload. Das ist nicht ganz trivial vor allem wenn wir ältere Geräte berücksichtigen wollen. Beachten muss man, dass man immer auf das Ergebnis warten muss bevor man den Upload startet.
https://www.askingbox.com/tutorial/how-to-resize-image-before-upload-in-browser https://github.com/nodeca/pica
Code Zusammengefaßt[Bearbeiten]
JS Basis für Drag and Drop[Bearbeiten]
// ************************ 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[Bearbeiten]
/* 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;
}