HTML5 Drag and Drop загрузка файлов

image
Благодаря нововведениям HTML5 создавать Drag and Drop интерфейсы стало гораздо проще. К сожалению, эти нововведения еще не обладают обширной поддержкой браузеров, но надеюсь в скором времени это изменится (на данный момент работает в Firefox 4+, Chrome и Opera 11.10).


Разметка


Сразу говорю, статья написана больше для новичков, чем для профессионалов. Поэтому некоторые моменты будут описываться очень подробно.

Для начала, нам необходимо создать HTML файл с таким содержанием:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
    
  <title>HTML5 Drag and Drop загрузка файлов</title>  
  <link rel="stylesheet" href="/style.css">
    
  <script src="//ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>
  <script src="/script.js"></script>
</head>
<body>    
    <form action="/upload.php">
      <div id="dropZone">
        Для загрузки, перетащите файл сюда.
      </div>
    </form>  
</body>
</html>


Вся работа у нас будет происходить с контейнером dropZone. Теперь добавим стили для нашего документа (style.css):

body {
    font: 12px Arial, sans-serif;
}

#dropZone {    
    color: #555;
    font-size: 18px;
    text-align: center;    
    
    width: 400px;
    padding: 50px 0;
    margin: 50px auto;
    
    background: #eee;
    border: 1px solid #ccc;
    
    -webkit-border-radius: 5px;
    -moz-border-radius: 5px;
    border-radius: 5px;
}

#dropZone.hover {
    background: #ddd;
    border-color: #aaa;
}

#dropZone.error {
    background: #faa;
    border-color: #f00;
}

#dropZone.drop {
    background: #afa;
    border-color: #0f0;
}


В стилях Вы можете заметить три состояния у элемента dropZone: при наведении, если возникает какая-то ошибка и при успешном выполнении.

Скрипт загрузки


Теперь мы приступим к самому интересному — написанию JavaScript кода. Для начала, нам необходимо создать переменные, в одну из которых мы поместим нашу dropZone, а во второй укажем максимальный размер файла.

$(document).ready(function() {
    var dropZone = $('#dropZone'),
        maxFileSize = 1000000; // максимальный размер файла - 1 мб.
});


Дальше мы должны проверить поддерживает ли браузер Drag and Drop, для этого мы будем использовать FileReader функцию. Если браузер не поддерживает Drag and Drop, то внутри элемента dropZone мы напишем «Не поддерживается браузером!» и добавим класс «error».

if (typeof(window.FileReader) == 'undefined') {
    dropZone.text('Не поддерживается браузером!');
    dropZone.addClass('error');
}


Следующее что мы сделаем это будет анимация эффекта перетаскивания файла на dropZone. Отслеживать эти действия мы будет с помощью событий «ondragover» и «ondragleave». Но, так как эти события не могут быть отслежены у jQuery объекта, нам необходимо обращаться не просто к «dropZone», а к «dropZone[0]».

dropZone[0].ondragover = function() {
    dropZone.addClass('hover');
    return false;
};
    
dropZone[0].ondragleave = function() {
    dropZone.removeClass('hover');
    return false;
};


Теперь нам необходимо написать обработчик события «ondrop» — это событие когда перетянутый файл опустили. В некоторых браузерах при перетягивании файлов в окно браузера они автоматически открываются, что бы такого не произошло нам нужно отменить стандартное поведение браузера. Также нам необходимо убрать класс «hover», и добавить класс «drop».

dropZone[0].ondrop = function(event) {
    event.preventDefault();
    dropZone.removeClass('hover');
    dropZone.addClass('drop');
};


Дальше нам нужно добавить проверку на размер файла, для этого добавим в обработчик «ondrop» следующий строчки кода:

var file = event.dataTransfer.files[0];
        
if (file.size > maxFileSize) {
    dropZone.text('Файл слишком большой!');
    dropZone.addClass('error');
    return false;
}


Теперь нам необходимо написать AJAX запрос отсылающий наш файл в обработчик. Для создания AJAX запроса мы будем использовать объект XMLHttpRequest. Добавим для объекта XMLHttpRequest два обработчика событий: один будет показывать прогресс загрузки файла, а второй результат загрузки. В качестве обработчика укажем файл upload.php.

var xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', uploadProgress, false);
xhr.onreadystatechange = stateChange;
xhr.open('POST', '/upload.php');
xhr.setRequestHeader('X-FILE-NAME', file.name);
xhr.send(file);


Теперь займемся функциями прогресса загрузки и результата загрузки. В функции «uploadProgress» нет ничего сложного, лишь простейшая математика.

function uploadProgress(event) {
    var percent = parseInt(event.loaded / event.total * 100);
    dropZone.text('Загрузка: ' + percent + '%');
}


В функции «stateChange» мы должны проверить завершен ли процесс загрузки, и если да, то необходимо проверить не возникла ли какая-либо ошибка. Код успешного запроса «200», если же код отличается от этого, то это означает, что произошла ошибка.

function stateChange(event) {
    if (event.target.readyState == 4) {
        if (event.target.status == 200) {
            dropZone.text('Загрузка успешно завершена!');
        } else {
            dropZone.text('Произошла ошибка!');
            dropZone.addClass('error');
        }
    }
}


Написание JavaScript части завершено.

Серверная часть


Все что нам осталось, это написать простейший обработчик, который будет сохранять файл в нужном нам месте. В написании обработчика я не буду сильно углубляться, а лишь приведу небольшой пример на PHP.

<?php

$uploaddir = getcwd().DIRECTORY_SEPARATOR.'upload'.DIRECTORY_SEPARATOR;
$uploadfile = $uploaddir.basename($_FILES['file']['name']);

move_uploaded_file($_FILES['file']['tmp_name'], $uploadfile);

?>


На этом всё, надеюсь статья была полезной для Вас.

Скачать исходные файлы вы можете здесь.
Перевод и доработка статьи: HTML5 Native Drag And Drop File Upload
Share post

Comments 61

    +3
    Пригодится, положил в копилку.
      0
      Для подсветки кода используйте тег source с атрибутом lang
        +2
        про HTML5 пишем, но HTML5 не используем :),
          0
          Тэг source на хабре к HTML5 отношения не имеет.
            0
              0
              …А, в статье? Так Хабр всё равно XHTML 1.0 Transitional.
              0
              Спасибо, поправил.
              +1
              А что, в Опере уже работает? Сразу пошел на gmail проверить, но там облом :(
              Может кто-нибудь бросить ссылку на страницу с примером drag&drop?
                0
                Нет, в Opera поддержки Drag And Drop пока нет.
                  0
                  и в IE, злит, так как из-за одного исключения возникают две формы интерфейса, оговорка в справке и т.д.
                0
                Удобно. Сейчас попытаюсь повторить у себя. Будем ждать спецификации HTML5. :)
                  0
                  Похоже на удобный способ выкачивать скрытно у пользователя его приватные файлы (пароли и т.п.) просто предлагая сыграть в puzzle-подобную игрушку. Под курсор ставим скрытый iframe с локальным file://URL и даем перетаскивать все подряд к нам. А если не только файлы, но и букмарки/прочее таким образом можно будет перетаскивать, то откроется большое поле для сбора приватной инфы.

                  Я не скептик, но как то с опаской смотрю на такие нововведения…
                    +1
                    Попробуйте так сделать и удивитесь =)
                      +1
                      Что-то мне подсказывает, что еще будут об этом писать…
                      Стандарты это хорошо, но реализация часто хромает. Если сейчас сделать все грамотно и перетаскивать локальные файлы будет не возможно, то завтра кто-то в браузер встроит плагин, где в окне будут открыты файлы из какого-нибудь cloud хранилища. И как фичу сделают возможность перетаскивать эти файлы (все события, dataTransfer интерфейсы и т.п.). Вдруг внезапно «откроется брешь в безопасности».

                      Нельзя предусмотреть все варианты, просто потому, что софт меняется. В последнее время браузеры стараются не спрашивать пользователя о скрытых запросах и процессах. На мой взгляд проблема реальна.

                      P.S. я думал карма ь это отношение участников к адекватности автора, а не оценка его взглядов. «Заминусованный» пользователь потеряет возможность писать, «неугодная» точка зрения исчезнет. Жалко смотреть во что превратили карму.
                        0
                        На сколько я помню доступ из внешнего фрейма запрещен к внутреннему если домены различаются.
                    0
                    вот так всегда =) когда уже почти дописал сам — на хабре вылезает статья с готовым решением
                      0
                      Вообще-то, тема drag-and-drop'а файлов освещалась на хабре ещё год назад.
                        0
                        404
                          0
                          Спасибо, очень полезный комментарий. Могли бы и правильную ссылочку приложить.

                          (Единственная информация, которой я воспользовался для нахождения правильной ссылки — это адрес исходной. Что мешало Вам воспользоваться гуглом? Хотя бы так?)
                            0
                            Извините, вы приложили битую ссылку, я указал вам на это, и вы этим недовольны? Мне кажется, вы неправы.
                              0
                              Вы на даты постов вообще смотрели? Ссылка вполне работала тогда, когда я её постил. Или Вы считаете, что я должен постоянно проходить все свои посты, следя за свежестью ссылок? Извините тогда, но я не вижу в этом смысла.

                              А недоволен я бессмысленностью Вашего комментария. А если бы я не увидел Вашего комментария? Юзер, если ему интересна ссылка, и сам заметит, что она умерла. А вот приведи Вы рабочую ссылку, Вы бы сэкономили его время и силы.
                                0
                                А вот на даты-то я и не посмотрел. Да, тогда неправ я. Честно говоря, мне не пришло в голову искать нормальную ссылку, как-то на автомате… Не работает, что теперь с этим поделать.

                                По-моему, сложно его не увидеть. Не знаю, как у вас, а мне хабра даже на мыло оповещаловку шлёт.
                                  0
                                  Уведомления-то и мне приходят (откуда ж иначе я заметил бы Ваш комментарий), но их можно отключить или проигнорировать.
                                    0
                                    Хм, а мы с вами на javascript.ru не пересекались ранее?
                                      0
                                      Пересекались, конечно же :-)
                                        0
                                        Мир тесен, приятно встретиться)
                      0
                      С Dropbox'a лучше удалите, могут временно заблокировать за трафик

                      И маленькие опечатки… :)
                      > максимальный размер фалйа
                      > Скачать исходны-Е- файлы вы можете здесь.

                      Ну а за статью спасибо, ради интереса опробуем на досуге ;)
                        0
                        Спасибо, исправил опечатки.

                        А куда посоветуете закинуть файлы, что бы хабр аудитория была довольна? :)
                          0
                          Яндекс.Диск подойдёт. Или свой хост, аккаунт на Google сайтах. Чем прямее ссылка, тем лучше :)
                            0
                            Угу, поправил ссылку. Залил на Яндекс.Диск.
                              0
                              Картинки еще живы :)?
                        +1
                        А зачем нужен заголовок X-FILE-NAME?
                        Если нужно передавать помимо файла другие значения, я бы посоветовал использовать FormData.
                          +1
                          Если используется jQuery, то код, в первую очередь аяксовую часть, можно было бы написать проще. Если не используется — тоже: можно было бы использовать getElementById и иметь на выходе сразу нужный узел. А Вы почему-то его используете исключительно для усложнения кода.
                            0
                            Я лишь перевел статью, кое-где что-то дописав или исправив.

                            Я думаю автор не использовал jQuery AJAX из-за двух функций «uploadProgress» и «stateChange». И я думаю, здесь главное не то, каким образом тут используется jQuery, а просто показать принцип работы Drag and Drop. А то как вы его будете реализовывать это (на чистом JavaScript или при помощью сторонних библиотек) — это уже ваше решение.
                              +1
                              Так вот я и говорю, что тогда уж использование jQuery из статьи стоило бы выбросить.
                                0
                                Кстати, в jQuery можно навешивать drag&drop обработчики событий. Так сделано в моей статье (которая на ту же тему как раз ;)
                              0
                              И почему большинство интересных штук придумывают за бугром?
                                0
                                Ну не скажите, я думаю у нас тоже есть чем гордится :)
                                  0
                                  У нас есть чем гордится, а «плюшки» придумывают всё равно за бугром.
                                0
                                Что здесь плохо, так это то, что при любом перемещении мыши с захваченным файлом над контейнером вы добавляете класс hover. При быстрых рывках мышью не наблюдается никаких артефактов?
                                Что будет, если я захвачу папку и брошу её?
                                  +2
                                  > Что будет, если я захвачу папку и брошу её?
                                  Ничего, массив files будет пустой, но можно выделить все файлы и перетащить их.

                                  Довольно хорошая демка(да сам скрипт): aquantum-demo.appspot.com/file-upload
                                  +1
                                  Обработчики на ondragover и ondragleave спокойно вешаются через jquery:
                                  $('#dropZone').bind('dragover', function(event){...}).bind('ondragleave', function(event){...})

                                  Раз файл передаются не как multipart запрос, то на стороне сервера массив $_FILES будет пустым и пример _не рабочий_, выхода 2:

                                  1) использовать FormData
                                  заменить:
                                  xhr.send(file)
                                  на:
                                  var fd = new FormData
                                  fd.append("file", file)
                                  xhr.send(fd)


                                  2) изменить серверную часть
                                  $input = fopen("php://input", "r");
                                  $target = fopen($path, "w");
                                  $realSize = stream_copy_to_stream($input, $target);
                                  fclose($input);
                                    0
                                    Несложно сделать выбор файла, добавив <input type="file" multiple="multiple" style="position:absolute;right:0;opacity:0;font-size:100px;"> в #dropZone:
                                    $('#dropZone input').change(function(){
                                    var file = this.files[0];
                                    ...
                                    });
                                      0
                                      вариант с php://input, к сожалению, дико жрет память (3x размер загружаемого файла)
                                        0
                                        Можно поконкретней, когда и с чем это проявляется, попробовал на 250м файле, ни apache2, ни ff, ни chrome не стали есть значительно больше памяти после начала загрузи.

                                        З. Ы. linux, загружал на localhost(возможно, не успевало буфферизоваться).
                                          +1
                                          я на это напоролся в ходе использования скрипта fileuploader.js, который по сути делает то же, что в статье написано.
                                          при загрузке мелких файлов все ок, при попытке залить файл на 200M php умирает из-за превышения memory_limit (стоял 512M).

                                          собственно, содержание серверного скрипта роли вообще не играет, можно <?php echo memory_get_usage();?> в качестве upload.php поставить и посмотреть вживую.
                                          при загрузке файла на 100K расход памяти где-то 400K, при загрузке 5.6M — уже 17M.

                                          php5.3.6, apache2+mod_php, ось debian 6.0 / win7.

                                          насколько я понимаю, проблема в том, что скрипт начинает исполняться только после полного получения тела запроса от клиента.
                                          в случае обычного multipart/form-data php на ходу раскладывает все загружаемые файлы в свой upload_tmp_dir, и в $_FILES пишет только пути к ним, при нестандартной же кодировке запроса php не знает, что с ним делать и все тело висит в памяти.
                                          вот зачем ему аж три копии — хз, в $HTTP_RAW_POST_DATA одна точно есть.
                                            0
                                            Провел еще раз эксперименты, процесс апача действительно съедает память, но если судить по top'у(ему я больше верю) — только в размере загружаемого файла, если по memory_get_usage(возващающему захваченную, но не факт что использованную память) — 3 размера файла, причем как с пустым скриптом так и с копированием php://input через stream_copy_to_stream
                                        0
                                        Спасибо, добрый человек. Всю голову сломал пока дошло, что стоит комменты прочитать. Автору поста — дислайк.
                                          0
                                          Спасибо за коммент!!!

                                          Действительно помогло!!!

                                          +2
                                          «maxFileSize = 1000000; // максимальный размер файла — 1 мб.» :)
                                            0
                                            maxFileSize = 1*1024*1024; поиллюстративнее будет.
                                              0
                                              Вообще говоря, 1 000 000 байтов — это действительно 1 мегабайт. 1 048 576 байтов — это уже 1 мебибайт, почти на 5% больше одного мегабайта.

                                              ru.wikipedia.org/wiki/Мегабайт
                                              0
                                              А насколько хороша поддержка загрузки больших файлов.
                                              Например 500MB с клиента потянет?
                                                +1
                                                Я думаю потянет. Но вопрос есть ли смысл? Возможно, для загрузки больших фалов стоит использовать что-то другое. Я думаю вариант с Drag and Drop больше подходит для более мелких файлов, например для картинок и музыки.
                                                  +3
                                                  D&D это всего лишь свойство контрола интерфейса, и в целом не имеет отношения к тому, как передается файл и что с ним вообще происходит.
                                                  И не всегда 500 мб через интеренет — это медленно.
                                                    0
                                                    Помимо самого D&D важен также способ отправки файлов. Стандартный XHR не умеет отправлять файлы (равно как и обычный JS даже не подозревает о существовании файлов), а стандарты пока находятся только в процессе разработки.
                                                    Так, например, в FF3.6 единственный способ отправить файл — отправить его содержимое в виде данных обычного (не multipart!) запроса (т.е. нужно получить весь файл одой строкой). Как результат — передать файл в 500мб, как минимум, затруднительно, т.к. чревато огромными затратами памяти.
                                                      0
                                                      Насчет FF3.6 вы немного ошибаетесь:

                                                      Starting with Gecko 1.9.2, you may also specify an DOM File

                                                      Т.е. начиная с FF3.6 (а это и есть Gecko 1.9.2) можно загружать файл не multipart-запросом, а просто содержимое файла в теле запроса, без чтения файла целиком в память.
                                                        0
                                                        Да, забыл про это.
                                                        Но это кривой способ, потому что файл на сервере нужно будет считывать из входного потока. Да и дополнительные параметры POST'ом не передать.
                                                0
                                                Рецепт успеха статьи на Хабре = перевод статьи из Mozilla developers hub + немного jQuery для аромата (а иначе, зачем его приплел автор, если даже для AJAX используется XHR?)
                                                  0
                                                  Успех скорее зависит от «модности» темы поднятой в статье.
                                                    0
                                                    Не, реально модные пацаны сейчас пишут про то, как нарисовать на HTML Canvas треугольник с градиентом или его же, вращающимся на WebGL

                                                Only users with full accounts can post comments. Log in, please.