От автора: данная статья написана нашим гостем Osvaldas Valutis. Osvaldas расскажет нам не только про drag and drop загрузку файлов на сервер, но и затронет тему UI и UX, поддержки браузеров, а также покажет, как реализовать данную загрузку с помощью метода прогрессивного улучшения.
Сейчас я работаю над RSS ридером Readerrr. И передо мной стояла задача разнообразить обычный способ добавления файлов через input, я хотел реализовать drag and drop модель. Иногда такой способ намного удобнее, разве не так?
Разметка
В разметке нет ничего особенного. Она обычная, просто тег form, хотя присутствуют также блоки с потенциальными состояниями.
1 2 3 4 5 6 7 8 9 10 |
<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> |
Пока блоки состояний нам не нужны, они прячутся:
1 2 3 4 5 6 |
.box__dragndrop, .box__uploading, .box__success, .box__error { display: none; } |
Разбираем код:
По состояниям: .box__uploading отображается во время загрузки файла через Ajax (все остальные блоки состояний скрыты). Затем в зависимости от результата загрузки отображаются .box__success или .box__error.
input[type=»file»] и label – функциональные элементы формы. В статье настройка input’ов типа file я описывал, как можно их стилизовать. Также в той статье я рассказывал, зачем нужен атрибут [data-multiple-caption]. Input и label служат альтернативой обычному способу выбора файлов (или единственный способ, если drag and drop не поддерживается).
.box__dragndrop отображается, если браузер поддерживает drag and drop.
Обнаружение свойств
Мы не можем 100% полагаться на поддержку в браузере drag and drop. Необходимо обеспечить фолбек, в этом нам поможет метод обнаружения поддерживаемых свойств. В основе метода drag and drop лежат множество различных JavaScript API, необходимо проверить их все.
Сначала само событие drag & drop. Работу по обнаружению поддерживаемых свойств можно с уверенностью доверить библиотеке Modernizr. Ниже тест события:
1 2 |
var div = document.createElement('div'); return ('draggable' in div) || ('ondragstart' in div && 'ondrop' in div) |
Далее необходимо проверить FormData интерфейс. Данный интерфейс формирует программный объект выбранного файла (ов), после чего он (и) могут быть отправлены на сервер через Ajax:
1 |
return 'FormData' in window; |
Также необходимо проверить объект DataTransfer. Тут немного хитрый метод проверки, так как не существует еще стопроцентного способа проверки на поддержку данного объекта до того, как пользователь не начнет взаимодействовать с drag and drop интерфейсом. Не все браузеры поддерживают объект. Вообще, необходимо избегать таких UX моментов типа:
«Перетащи файл сюда!»
Пользователь перетащил и отпустил файл и
«Опа, я пошутил, такая функция не поддерживается.»
Основная задача успеть проверить поддержку FileReader API в момент загрузки документа. Смысл в том, что если браузер поддерживает FileReader, то и DataTransfer он тоже поддерживает:
1 |
'FileReader' in window |
Добавим код выше в анонимную самовызывающуюся функцию…
1 2 3 4 |
var isAdvancedUpload = function() { var div = document.createElement('div'); return (('draggable' in div) || ('ondragstart' in div && 'ondrop' in div)) && 'FormData' in window && 'FileReader' in window; }(); |
…с ее помощью можно четко определить поддержку свойств:
1 2 3 |
if (isAdvancedUpload) { // ... } |
С полностью рабочим методом обнаружения поддерживаемых свойств, мы можем сказать пользователю, может ли он воспользоваться drag and drop или нет. В случае поддержки к форме можно добавить специальный класс, чтобы потом стилизовать форму:
1 2 3 4 5 |
var $form = $('.box'); if (isAdvancedUpload) { $form.addClass('has-advanced-upload'); } |
1 2 3 4 5 6 7 8 |
.box.has-advanced-upload { background-color: white; outline: 2px dashed black; outline-offset: -10px; } .box.has-advanced-upload .box__dragndrop { display: inline; } |
Не беда если drag & drop не поддерживается. Пользователи могут загружать файлы через старый добрый input[type=»file»]!
Обратите внимание: в Microsoft Edge есть баг, после которого drag & drop не работает. Разработчики вроде бы уже знают о нем, и работают над исправлением.
Drag ‘n’ Drop
А теперь займемся делом. В этой части мы будем добавлять к форме или удалять специальные классы состояний типа, когда пользователь держит элемент над формой. Когда пользователь будет отпускать кнопку мыши, мы будем ловить данные файлы.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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() и e.stopPropagation() предотвращают любые нежелательные действия конкретных событий в браузере.
e.originalEvent.dataTransfer.files возвращает список файлов. Позже я покажу, как использовать эту информацию для отправки файлов на сервер.
С помощью класса .is-dragover мы будем указывать пользователю, когда можно отпустить файлы:
Стандартный способ выбора файлов
Иногда drag & drop не самый удобный способ выбора файлов. Особенно если у пользователя маленький экран. А значит, необходимо дать пользователю выбор между различными методами загрузки. В этом нам поможет input типа file и label. Стилизовав их описанным мной способом, можно сохранить целостность дизайна:
Ajax загрузка
Не существует полностью кроссбраузерного способа реализовать drag & drop без Ajax. Некоторые браузеры (IE и Firefox) не позволяют устанавливать значение в input’ах типа file, которые потом могут отправляться на сервер. Код ниже не работает:
1 |
$form.find('input[type="file"]').prop('files', droppedFiles); |
Вместо кода выше после отправки формы необходимо использовать Ajax:
1 2 3 4 5 6 7 8 9 10 11 |
$form.on('submit', function(e) { if ($form.hasClass('is-uploading')) return false; $form.addClass('is-uploading').removeClass('is-error'); if (isAdvancedUpload) { // ajax для современных браузеров } else { // ajax для старых браузеров } }); |
У класса .is-uploading двойное значение: он предотвращает повторную отправку формы (return false), а также показывает пользователям процесс отправки формы:
1 2 3 4 5 6 |
.box.is-uploading .box__input { visibility: none; } .box.is-uploading .box__uploading { display: block; } |
Ajax для современных браузеров
Если бы в форме не было загрузки файлов, то нам бы и не понадобилось два разных Ajax способа. Но к сожалению, в IE9 и ниже не поддерживается загрузка через XMLHttpRequest.
Чтобы понять, какой метод поддерживается, можно воспользоваться нашим готовым тестом isAdvancedUpload. Так как если браузер поддерживает то, о чем я писал выше, то он будет поддерживать загрузку через XMLHttpRequest. Код ниже работает в IE10+:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
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() { // Сохраняйте ошибки в логи, показывайте предупреждения, что угодно } }); } |
FormData($form.get(0)) собирает данные из всех input’ов.
Цикл $.each() пробегается по всем сброшенным файлам. ajaxData.append() добавляет эти файлы в стек для отправки через Ajax.
data.success и data.error – JSON строки с результатом выполнения, возвращенные сервером. Ниже показано, как это будет выглядеть на PHP:
1 2 3 4 |
<?php // ... die(json_encode([ 'success'=> $is_success, 'error'=> $error_msg])); ?> |
Ajax для старых браузеров
Метод для IE9-. Нам не нужно собирать drag & drop файлы (isAdvancedUpload = false), так как данный метод не поддерживается браузером. Форма работает через input[type=»file»]. Как ни странно, динамическая вставка iframe работает:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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(); }); } |
Автоматическая отправка файлов
Если в вашей форме только drag & drop поле или input типа file, для удобства пользователя можно сделать, чтобы файлы автоматически загружались на сервер без нажатия на кнопку отправить. Для этого необходимо вручную запустить событие submit:
1 2 3 4 5 6 7 8 9 10 11 12 |
// ... .on('drop', function(e) { // drag & drop поддерживается droppedFiles = e.originalEvent.dataTransfer.files; $form.trigger('submit'); }); // ... $input.on('change', function(e) { // drag & drop НЕ поддерживается $form.trigger('submit'); }); |
Если хорошо спроектировать область drag & drop (пользователю будет очевидно, что от него хотят), можно вообще скрыть кнопку отправки (иногда, чем меньше интерфейса, тем лучше). Но будьте осторожны. Если по каким-то причинам JavaScript отключен, кнопка должна быть видимой (прогрессивное улучшение!). Чтобы понять, включен ли JS, можно воспользоваться классом .no-js в теге html:
1 2 3 4 5 6 |
<html class="no-js"> <head> <!—если используете 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> |
1 2 3 4 5 6 |
.box__button { display: none; } .no-js .box__button { display: block; } |
Отображение выбранных файлов
Если вы не используете метод автоотправки файлов на сервер, то необходимо показать пользователю, что он успешно выбрал файлы для загрузки:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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); }); |
Когда JavaScript отключен
Основной принцип прогрессивного улучшения в том, что пользователь должен любым способов закончить принципиальную задачу, неважно каким. И загрузка файлов не исключение. Если по каким-либо причинам JavaScript не доступен, интерфейс должен выглядеть так:
При нажатии на кнопку отправки формы, страница обновится. Так как JS отключен, то с его помощью нельзя показать результат отправки. Тут необходимо положиться на сервер. Ниже показан пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<?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; */ } ?> |
И немного правок в разметке:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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> |
Вот и все! Эта и так долгая статья могла быть еще длиннее. Но я думаю, этого вполне хватит, чтобы вы начали использовать адаптивную drag & drop загрузку в своих проектах. Чтобы более подробно ознакомиться с принципом работы, изучите демо (в исходниках можно посмотреть no-jQuery зависимости).
Автор: Osvaldas Valutis
Источник: //css-tricks.com/
Редакция: Команда webformyself.
Комментарии (1)