Drag and Drop загрузка файлов на сервер

Drag and Drop загрузка файлов на сервер

От автора: данная статья написана нашим гостем Osvaldas Valutis. Osvaldas расскажет нам не только про drag and drop загрузку файлов на сервер, но и затронет тему UI и UX, поддержки браузеров, а также покажет, как реализовать данную загрузку с помощью метода прогрессивного улучшения.

Сейчас я работаю над RSS ридером Readerrr. И передо мной стояла задача разнообразить обычный способ добавления файлов через input, я хотел реализовать drag and drop модель. Иногда такой способ намного удобнее, разве не так?

Демо

Разметка

В разметке нет ничего особенного. Она обычная, просто тег form, хотя присутствуют также блоки с потенциальными состояниями.

<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&hellip;</div>
  <div class="box__success">Done!</div>
  <div class="box__error">Error! <span></span>.</div>
</form>

Пока блоки состояний нам не нужны, они прячутся:

.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. Ниже тест события:

var div = document.createElement('div');
return ('draggable' in div) || ('ondragstart' in div && 'ondrop' in div)

Далее необходимо проверить FormData интерфейс. Данный интерфейс формирует программный объект выбранного файла (ов), после чего он (и) могут быть отправлены на сервер через Ajax:

return 'FormData' in window;

Также необходимо проверить объект DataTransfer. Тут немного хитрый метод проверки, так как не существует еще стопроцентного способа проверки на поддержку данного объекта до того, как пользователь не начнет взаимодействовать с drag and drop интерфейсом. Не все браузеры поддерживают объект. Вообще, необходимо избегать таких UX моментов типа:

«Перетащи файл сюда!»

Пользователь перетащил и отпустил файл и

«Опа, я пошутил, такая функция не поддерживается.»

Основная задача успеть проверить поддержку FileReader API в момент загрузки документа. Смысл в том, что если браузер поддерживает FileReader, то и DataTransfer он тоже поддерживает:

'FileReader' in window

Добавим код выше в анонимную самовызывающуюся функцию…

var isAdvancedUpload = function() {
  var div = document.createElement('div');
  return (('draggable' in div) || ('ondragstart' in div && 'ondrop' in div)) && 'FormData' in window && 'FileReader' in window;
}();

…с ее помощью можно четко определить поддержку свойств:

if (isAdvancedUpload) {
  // ...
}

С полностью рабочим методом обнаружения поддерживаемых свойств, мы можем сказать пользователю, может ли он воспользоваться drag and drop или нет. В случае поддержки к форме можно добавить специальный класс, чтобы потом стилизовать форму:

var $form = $('.box');

if (isAdvancedUpload) {
  $form.addClass('has-advanced-upload');
}
.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

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

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, которые потом могут отправляться на сервер. Код ниже не работает:

$form.find('input[type="file"]').prop('files', droppedFiles);

Вместо кода выше после отправки формы необходимо использовать Ajax:

$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), а также показывает пользователям процесс отправки формы:

.box.is-uploading .box__input {
  visibility: none;
}
.box.is-uploading .box__uploading {
  display: block;
}

Ajax для современных браузеров

Если бы в форме не было загрузки файлов, то нам бы и не понадобилось два разных Ajax способа. Но к сожалению, в IE9 и ниже не поддерживается загрузка через XMLHttpRequest.

Чтобы понять, какой метод поддерживается, можно воспользоваться нашим готовым тестом isAdvancedUpload. Так как если браузер поддерживает то, о чем я писал выше, то он будет поддерживать загрузку через XMLHttpRequest. Код ниже работает в IE10+:

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:

<?php
  // ...
  die(json_encode([ 'success'=> $is_success, 'error'=> $error_msg]));
?>

Ajax для старых браузеров

Метод для IE9-. Нам не нужно собирать drag & drop файлы (isAdvancedUpload = false), так как данный метод не поддерживается браузером. Форма работает через input[type="file"]. Как ни странно, динамическая вставка iframe работает:

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:

// ...

.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:

<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>
.box__button {
  display: none;
}
.no-js .box__button {
  display: block;
}

Отображение выбранных файлов

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

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 отключен, то с его помощью нельзя показать результат отправки. Тут необходимо положиться на сервер. Ниже показан пример:

<?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;
    */
  }

?>

И немного правок в разметке:

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

Источник: https://css-tricks.com/

Редакция: Команда webformyself.

Курс по программированию на языке PHP

Изучите PHP с нуля до результата!

Смотреть курс

Метки:

Комментарии Вконтакте:

Комментарии Facebook:

Комментарии (1)

  1. Alex

    не сработало добавление файлов в форму средствами JS.
    сработало вот так:

    if( droppedFiles ){
    jQuery.each( droppedFiles, function( i, file ){
    ajaxData.append('file[]', file);
    });
    }

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Можно использовать следующие HTML-теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Я не робот.

Spam Protection by WP-SpamFree