Wgrywanie plików za pomocą techniki drag & drop (HTML5)
Biblioteka JavaScript wykorzystująca możliwości HTML5
08 Listopad 2010
Autor: Krzysztof Wilczek / Weeby.pl
Możliwość wykorzystania obiektu FileReader do zapewnienia funkcjonalności wgrywania plików metodą drag & drop istnieje w przeglądarkach Firefox 3.6 (silnik Gecko >=1.9.2) oraz Google Chrome. Safari także oferuje pewną możliwość wgrywania plików wykorzystując obiekt typu input strony internetowej. Obiekt FileReader pozwala na asynchroniczny odczyt zawartości plików przechowywanych na komputrze użytkownika. Do odczytanych plików możemy uzyskać dostęp dzięki FileList (obiektowi zwracanemu jako rezultat wyboru plików za pomocą elementu <input>) oraz dzięki obiektowi DataTransfer (zwracanemu w rezultacie operacji typu drag & drop). Łącząc FileReader w zastosowaniu z DataTransfer możemy zbudować niezwykle intuicyjny upload plików z lokalnego komputera na serwer, polegający na zwykłym przeciągnięciu ich z wybranego folderu do odpowiedniego elementu witryny internetowej.
Ponieważ trzy liczące się na rynku przeglądarki: Chrome, Firefox oraz Safari udostępniły pewne metody pobierania plików jednak każda z nich w inny sposób zdecydowaliśmy się opracować uniwersalną klasę pozwalającą na upload plików graficznych na serwer. Dzięki przygotowanemu skryptowi inteferejs użytkownika witryny internetowej może stać się bardziej przyjaźny i efektowny.
Przykład zastosowania biblioteki html5uploader
Online Demo | Wersja do ściągnięcia | Biblioteka na Code Google
Rozpoczynamy od przygotowania prostej strony HTML 5, na której osadzony będzie obiekt klasy realizującej drag & drop obrazów.
Przewidzieć należy trzy elementy właściwej zawartości strony: miejsce dla przerzucania plików <div id=”drop”>, miejsce na wyprowadzenie statusów operacji działania na plikach <div id=”status”> oraz miejsce na prezentację wyników <div id=”list”>. Czwarta ramka <div id=”box”> służy grupowaniu elementów i może mieć zastosowanie dla estetyki prezentacji strony.
Poniższy kod przedstawia stronę HTML 5:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>HTML5</title> <script src="html5uploader.js"></script> </head> <body> <div id="box"> <div id="status">Przeciągnij plik z lokalnego folderu do pojemnika ...</div> <div id="drop"></div> </div> <div id="list"></div> </body> </html>
Jak łatwo moża zauważyć, jako biblioteka JS podpięty został plik html5uploader.js zawierający przygotowywaną przez Weeby klasę. Zakładamy, że obiekt będzie utworzony po załadowaniu elementów witryny. W tym celu rozbudowujemy element body strony o wywołanie funkcji onload.
<body onload="new uploader('drop', 'status', '/uploader.php', 'list');">
Konstruktor obiektu przyjmuje cztery atrybuty: uploader(place, status, targetPHP, show). Atrybut „place” wskazuje element strony do którego będą przeciągane pliki. „Status” wskazuje miejsce wyświetlania statusów operacji jakie będzie realizował obiekt. Pobrane pliki muszą zostać zapisane na serwerze dzięki PHP, stąd należy wskazać skrypt PHP realizujący zadanie zapisu („targetPHP”). Ostatni atrybut „show” wskazuje element strony w ramach którego prezentowane mają być pobrane obrazy.
Możemy pominąć podawanie miejsc wyświetlania statusów oraz rezultatów działania skryptu ograniczając wywołanie w poniższy sposób.
uploader = new uploader('drop', null, '/uploader.php', null);
Odbieranie plików po stronie PHP
Aby móc zapisać pliki pobrane przez przeglądarkę potrzebny jest skrypt PHP zapisujący obrazy w wybranej lokalizacji. Jeśli przeglądarka obsługuje funkcje sendAsBinary() można zwyczajnie wykorzystać tablicę $_FILES i przenieść pobrane pliki z folderu tymczasowego do wskazanej lokalizacji. W pozostałych przypadkach należy zastososować base64_decode oraz file_get_contents w celu pobrania zawartości binarnej plików. Za pomocą funkcji file_put_contents() tworzymy plik w określonej lokalizacji z pobraną wcześniej zawartością.
Poniższy skrypt PHP prezentuje proste pobranie plików
<?php
$upload_folder = 'data';
if(count($_FILES)>0) {
if( move_uploaded_file( $_FILES['upload']['tmp_name'] , $upload_folder.'/'
.$_FILES['upload']['name'] ) ) {
echo 'done';
}
exit();
} else if(isset($_GET['up'])) {
if(isset($_GET['base64'])) {
$content = base64_decode(file_get_contents('php://input'));
} else {
$content = file_get_contents('php://input');
}
$headers = getallheaders();
$headers = array_change_key_case($headers, CASE_UPPER);
if(file_put_contents($upload_folder.'/'.$headers['UP-FILENAME'], $content)) {
echo 'done';
}
exit();
}
?>
Dzięki zastosowaniu powyższych skryptów JS oraz PHP w prosty sposób uzyskujemy możliwość wgrywania plików na serwer porzez proste przeciągnięcie ich z wybranego folderu na komputerze użytkownika.
Budowa klasy html5uploader
Konstrukcję klasy rozpoczynamy od zbudowania tzw. EventListenerów przechwytujących zdarzenia „dragover” oraz „drop” dla wskazanego elementu strony (miejsca do przeciągania obrazów).
function uploader(place, status, targetPHP, show) {
upload = function(file) {
}
this.drop = function(event) {
}
this.uploadPlace = document.getElementById(place);
this.uploadPlace.addEventListener("dragover", function(event) {
event.stopPropagation();
event.preventDefault();
}, true);
this.uploadPlace.addEventListener("drop", this.drop, false);
}
Podczas zdarzenia „dragover” (czyli przeciągania ponad) klasa nie wykonuje zasadniczo żadnych operacji. Interesującym jest jednak zdarzenie „drop”, którego wystąpienie uruchomi funkcję klasy. Funkcja drop odpowiedzialna będzie za użycie obiektu DataTransfer i pobieranie listy przeciągniętych plików. Funckja „upload” spowoduje przetworzenie listy i jej asynchroniczne wysłanie do serwera.
Poniższy skrypt przedstawia szczegółowo rozpisaną funkcję drop
this.drop = function(event) {
event.preventDefault();
var dt = event.dataTransfer;
var files = dt.files;
for (var i = 0; i < files.length; i++)
{
var file = files[i];
upload(file);
}
}
Funkcja „drop” przechywtuje zdarzenie upuszczenia elementów nad wskazanym elementem strony. Pierwszą czynnością jaką wykonuje funkcja jest zablokowanie naturalnego dla przeglądarki zdarzenia event.preventDefault(); . Normalnie bowiem, po przeciągnięciu obrazu nad stronę www zostałby on zwyczajnie wyświetlony w oknie przeglądarki. Kolejną czynnością jest wykorzystanie obiektu dataTransfer i pobranie listy plików. Dla każdego pliku z listy wywołana zostaje funkcja upload().
upload = function(file) {
// Firefox 3.6, Chrome 6, WebKit
if(window.FileReader) {
this.loadEnd = function() {
}
this.loadError = function(event) {
}
this.loadProgress = function(event) {
}
this.previewNow = function(event) {
}
reader = new FileReader();
// Firefox 3.6, WebKit
if(reader.addEventListener) {
reader.addEventListener('loadend', this.loadEnd, false);
if (status)
{
reader.addEventListener('error', this.loadError, false);
reader.addEventListener('progress', this.loadProgress, false);
}
// Chrome 7
} else {
reader.onloadend = this.loadEnd;
if (status)
{
reader.onerror = this.loadError;
reader.onprogress = this.loadProgress;
}
}
var preview = new FileReader();
// Firefox 3.6, WebKit
if(preview.addEventListener) {
preview.addEventListener('loadend', this.previewNow, false);
// Chrome 7
} else {
preview.onloadend = this.previewNow;
}
reader.readAsBinaryString(file);
if (show) {
preview.readAsDataURL(file);
}
}
}
Pierwszym krokiem działania funkcji jest weryfikacja czy przeglądarka obsługuje obiekt FileReader z pomocą warunku if (window.FileReader) { instrukcje }. Jeśli przeglądarka obsługuje FileReader można wykorzystać jego zdarzenia. W tym miejscu jednak należy uwzględnić typ przeglądarki: dla Firefox 3.6 oraz WebKit możemy użyć reader.addEventListener, a w przypadku Google Chrome 7 musimy wprost zdefiniować funkcje np. reader.onloadend. Obiekt FileReader posiada zaimplementowane 3 metody: readAsBinaryString, readAsDataURL oraz readAsText. Nie trudno domyślić się, że ostatnia z nich odpowiada za odczyt tekstu z plików. Metoda readAsDataURL pozwala na utworzenie tzw. blob URL. W praktyce metoda ta daje możliwość utworzenia obiektu <img> na stronie www oraz ustawienie mu jako atrybtu „src” zawartości pobranej przez FileReader.
Obiekt FileReader w przedstawionym wyżej skrypcie został zostosowany dwukrotnie. Raz służy do pobrania binarnych danych pliku obrazu (reader) i przesłaniu ich na serwer, drugi raz (preview) wykorzystany jest do wyświetlenia podglądu obrazu, jeśli użytkownik wskazał miejsce na wyświetlenie rezultatu „show”.
Jeśli tworząc obiekt podany zostanie atrybut „status” poza zdarzeniem onloadend obsłużone zostaną także zdarzenia onerror oraz onprogress sygnalizujące kolejne etapy działania obiektu FileReader.
Poniższy skrypt prezentuje szczegółowo rozpisaną metodę loadError, której zadaniem jest obsługa sygnalizacji błędów obiektu FileReader.
this.loadError = function(event) {
switch(event.target.error.code) {
case event.target.error.NOT_FOUND_ERR:
document.getElementById(status).innerHTML = 'Pliku nie odnaleziono!';
break;
case event.target.error.NOT_READABLE_ERR:
document.getElementById(status).innerHTML = 'Plik jest nieodczytywalny!';
break;
case event.target.error.ABORT_ERR:
break;
default:
document.getElementById(status).innerHTML = 'Błąd odczytu.';
}
}
Drugą metodą stosowaną dla prezentacji stanu pobierania plików obrazów jest loadProgress(event). Ma ona znaczenie przy duży plikach określając procentową wielkość danych pobranego obrazu.
this.loadProgress = function(event) {
if (event.lengthComputable) {
var percentage = Math.round((event.loaded * 100) / event.total);
$(status).innerHTML = 'Załadowano : '+percentage+'%';
}
}
Kluczową funkcją obiektu jest loadEnd powodujący asynchroniczne wysłanie plików obrazów do serwera.
this.loadEnd = function() {
bin = reader.result;
xhr = new XMLHttpRequest();
xhr.open('POST', targetPHP+'?up=true', true);
var boundary = 'xxxxxxxxx';
var body = '--' + boundary + "\r\n";
body += "Content-Disposition: form-data; name='upload'; filename='" + file.name + "'\r\n";
body += "Content-Type: application/octet-stream\r\n\r\n";
body += bin + "\r\n";
body += '--' + boundary + '--';
xhr.setRequestHeader('content-type', 'multipart/form-data; boundary=' + boundary);
if(xhr.sendAsBinary != null) {
xhr.sendAsBinary(body);
} else {
xhr.open('POST', targetPHP+'?up=true&base64=true', true);
xhr.setRequestHeader('UP-FILENAME', file.name);
xhr.setRequestHeader('UP-SIZE', file.size);
xhr.setRequestHeader('UP-TYPE', file.type);
xhr.send(window.btoa(bin));
}
if (show) {
var newFile = document.createElement('div');
newFile.innerHTML = 'Załadowano : '+file.name+' wielkości '+file.size+' B';
document.getElementById(show).appendChild(newFile);
}
if (status) {
document.getElementById(status).innerHTML = 'Załadowano : 100%
Next file ...';
}
}
Metoda ta odpowiada za utworzenia obiektu XMLHttpRequest i przesłanie danych pobranych przez FileReader. W przypadku FireFox 3.6 możliwym jest zastosowanie funkcji sendAsBinary pozwalającej na przesłanie danych binarnych pobranego pliku. Funkcja ta nie jest jednak dostępna w przeglądarce Chrome 7 dlatego będziemy musieli zastosować metodę window.btoa() oraz funkcję kodującą base64 po stronie PHP. W przypadku jeśli zostały podane parametry „show” oraz „status” metoda przedstawi także wyniki swojej pracy.
Ponadto w przypadku określenia miejsca „show” pobranych obrazów zostaną one załadowane na stronie za pomocą metody previewNow().
this.previewNow = function(event) {
bin = preview.result;
var img = document.createElement('img');
img.className = 'addedIMG';
img.file = file;
img.src = bin;
document.getElementById(show).appendChild(img);
}
Obsługa wgrywania plików w Safari
Dobrym obyczajem jest rozszerzanie funkcjonalności obiektów tak aby były możliwie kompatybilne z szeroką gamą przeglądarek internetowych i w każdej z nich zwracały podobne rezultaty działania. Niestety jednak w przypadku obiektu FileReader nie został on, ani jego odpowiednik, zaimplementowany w przeglądarkach rodziny Internet Explorer oraz Opera. Możliwe jest jadnak proste rozbudowanie klasy dla przegląderk Safari 5+ obsługujące pobieranie plików. Wystarczy dla warunku if(window.FileReader) { instrukcje } dopisać blok else {} umieszczając w nim poniższe instrukcje.
} else {
xhr = new XMLHttpRequest();
xhr.open('POST', targetPHP+'?up=true', true);
xhr.setRequestHeader('UP-FILENAME', file.name);
xhr.setRequestHeader('UP-SIZE', file.size);
xhr.setRequestHeader('UP-TYPE', file.type);
xhr.send(file);
if (status) {
document.getElementById(status).innerHTML = 'Załadowano : 100%';
}
if (show) {
var newFile = document.createElement('div');
newFile.innerHTML = 'Załadowano : '+file.name+' wielkości '+file.size+' B';
document.getElementById(show).appendChild(newFile);
}
}
Tym sposobem klasa uplodera typu drag & drop stała się kompatybilna także z rodziną przeglądarek Safari 5.
[...] wiadomości z tego serwisu Follow us on Twitter 77 śledzących RSS Feed 383 czytelników Wgrywanie plików za pomocą drag & drop w HTML5/JS 1 głosuj! Opis działania biblioteki, która pozwala na dodanie funkcjonalności [...]
Google Chrome now supports FormData (http://www.w3.org/TR/XMLHttpRequest2/#the-formdata-interface), which allows to send binary data without using base64. I think it would be better to use FormData instead of encoding into base64.
P.S. Sorry for my english.
Hello, could you update the code for Chrome 8. I have to use your code but for large files, there are problems. Thanks for your help !
Witam. Świetny tutorial. Mam tylko jeden problem, gdy przeciągam pojedynczo pliki jest wszystko ok. Natomiast gdy zaznaczę więcej plików i wrzucę to w podglądzie jest poprawnie natomiast na serwerze jest ten sam plik tylko z różnymi nazwami. Tak jakby dodał jeden plik a następne tworzył z tego pierwszego a zmieniał tylko nazwy.
Witam! Problem najprawdopodobniej spowodowany jest błędem w 92 linii kodu. Dla poprawnego działania skrytpu należy zamienić deklarację reader = new FileReader(); na var reader = new FileReader();
Dziękuję. Pomogło:)
[...] html5uploader [...]
Nie działa wgrywanie plików na serwer.
Gdy przeciągam pliki do okna przeglądarki nie pojawiają się one na serwerze.
Hi, this uploader works very well. All data are uploaded correctly even uploading big files.
However I found a problem: status field doesn’t display real percentage of uploaded data. Users need to know when file upload is complete. How can I solve?
Thanks a lot
The problem is caused by the fact that the percentage of progress refers to only the progress of FileReader object. To get the real data must take into account the sending of a file from the browser to the server via XMLHttpRequest.