Реализация Drag & Drop при загрузке файлов на VueJS

Spread the love

Иногда когда дело доходит до загрузки файлов на сайт, перетаскивание файлов (Drag & Drop) из каталога является одним из наиболее удобных способов выполнить эту задачу. Практически любое современное веб-приложение реализует этот функционал. До этого момента мы описали разные приемы загрузки файлов с помощью VueJS и Axios. Была статья о стандартной загрузки файлов, где пользователи могут загрузить один файл или несколько файлов: Загрузка файлов с помощью VueJS и Axios, была статья о создание строки состояния процесса загрузки: Индикатор хода загрузки файла с Axios и VueJS и была статья о создание предварительного просмотра файлов: Предварительный просмотр загрузки файлов с помощью Axios и VueJS.

В этом уроке мы собираемся объединить все эти знания, чтобы сделать собственный загрузчик файлов с VueJS и Axios. Мы позволим пользователям выбирать любое количество файлов, удалять те, которые им не нужны, показывать превью изображений и отображать строку состояния во время загрузки файлов. Пользователь сможет выбрать, какие файлы он хочет загрузить с помощью перетаскивания (Drag & Drop) или стандартного выбора файлов.

Большая часть функциональности перетаскивания пришла от Освальдаса Валутиса и его поста о хитростях CSS: Drag and Drop File Uploading | CSS-Tricks. Я адаптировал то, что нам нужно, чтобы это работало внутри компонента Vue, а не с помощью jQuery, и добавил некоторые навороты из последних двух статей.

Этот урок будет проходить по шагам:
1. Создадим базу для перетаскивания (Drag & Drop).
2. Добавить превью для выбранных изображений.
3. Разрешим пользователям удалять файлы, которые им больше не нужны.
4. Реализуем загрузку выбранных файлов.
5. Добавим индикатор загрузки.
6. Реализуем небольшую функцию для процесса прямой загрузки. Это позволит сразу же загружать файлы, при перетаскивание.

Создание основы для перетаскивания

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

Во-первых, нам нужно создать пустой компонент Vue. В этом компоненте добавьте следующую заглушку:

<style>
  form{
    display: block;
    height: 400px;
    width: 400px;
    background: #ccc;
    margin: auto;
    margin-top: 40px;
    text-align: center;
    line-height: 400px;
      border-radius: 4px;
  }
</style>

<template>
  <div id="file-drag-drop">
    <form ref="fileform">
        <span class="drop-files">Drop the files here!</span>
    </form>
  </div>
</template>

<script>
  export default {

  }
</script>

На основе этого мы будем строить всю нашу функциональность. Я добавил немного стилей для формы, которые просто центрирует ее по экрану, и добавил текст, чтобы указать пользователям, куда перетаскивать файлы. Osvaldas описал прекрасный UX в своем уроке: Drag and Drop File Uploading | CSS-Tricks. Я же сосредоточусь только на реализации некоторых функций на VueJS и Axios. Не стесняйтесь стилизовать элементы в любом случае!

Единственная реальная вещь, о которой стоит упомянуть о шаблоне — это атрибут ref в теге <form>. Он позволит нам получить доступ к элементу из нашего кода компонента.

Убедитесь, что браузер поддерживает перетаскивание

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

Во-первых, давайте добавим поле data к нашему компоненту с флагом, чтобы увидеть, поддерживает ли браузер перетаскивание:

data(){
  return {
    dragAndDropCapable: false,
      files: []
  }
},

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

Далее добавим метод, который определяет, поддерживает ли браузер перетаскивание. Добавьте следующий метод к объекту methods в компоненте Vue:

determineDragAndDropCapable(){
  var div = document.createElement('div');

  return ( ( 'draggable' in div )
          || ( 'ondragstart' in div && 'ondrop' in div ) )
          && 'FormData' in window
          && 'FileReader' in window;
}

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

Мы также проверяем присутствуют ли следующие объекты в window , FormData (FormData – Web APIs | MDN) и FileReader (FileReader – Web APIs | MDN) для полной функциональности. Если все они существуют, мы вернем true из метода и продолжим инициализацию.

Инициализируем наш компонент через hook жизненного цикла mount()

Мы будем использовать функциональность метода determineDragAndDropCapable() в хуке жизненного цикла mounted(), чтобы определить, следует ли нам добавлять event listeners.

Давайте сначала добавим наш хук жизненного цикла и инициализаторы следующим образом:

mounted(){
  this.dragAndDropCapable = this.determineDragAndDropCapable();

  if( this.dragAndDropCapable ){
    ['drag', 'dragstart', 'dragend', 'dragover', 'dragenter', 'dragleave', 'drop'].forEach( function( evt ) {
      this.$refs.fileform.addEventListener(evt, function(e){
        e.preventDefault();
        e.stopPropagation();
      }.bind(this), false);
    }.bind(this));

    this.$refs.fileform.addEventListener('drop', function(e){
      for( let i = 0; i < e.dataTransfer.files.length; i++ ){
        this.files.push( e.dataTransfer.files[i] );
      }
    }.bind(this));
  }
},

Самая важная причина использования mounted() hook — это запуск после вставки шаблона в HTML. Он позволяет нам получить доступ к ссылке на форму, которую мы создали при создании нашего шаблона.

Сначала мы устанавливаем наш локальный флаг dragAndDropCapable на то, что возвращается из нашего метода:

this.dragAndDropCapable = this.determineDragAndDropCapable();

Он определяет, должны ли мы продолжать связывать события, которые мы ищем, в поле формы или нет. Нет смысла связывать события, которых не существует!

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

['drag', 'dragstart', 'dragend', 'dragover', 'dragenter', 'dragleave', 'drop'].forEach( function( evt ) {
  this.$refs.fileform.addEventListener(evt, function(e){
    e.preventDefault();
    e.stopPropagation();
  }.bind(this), false);
}.bind(this));

Здесь мы выполняем итерацию по массиву событий, связанных с перетаскиванием. Затем мы привязываем прослушиватель событий к каждому событию в форме, к которой мы обращаемся, с помощью нашего глобального массива $refs. Что мы делаем с событиями ?…, так это предотвращаем действие по умолчанию, которое заключается в открытии файлов, перетаскиваемых в браузер в новом теге. И затем мы прекращаем распространение события, чтобы другие элементы не решили, что они откроют файлы в новой вкладке.

Теперь нам нужно прослушать событие, чтобы обнаружить файлы, которые они сбрасывают. Для этого нам нужно добавить listener событий в события drop к форме:

this.$refs.fileform.addEventListener('drop', function(e){
  for( let i = 0; i < e.dataTransfer.files.length; i++ ){
    this.files.push( e.dataTransfer.files[i] );
  }
}.bind(this));

Есть несколько функций, на которые нужно обратить внимание с этим слушателем событий. Сначала мы связываем локальный компонент с помощью метода .bind(this) с функцией, с которой мы обрабатываем событие drop. Это дает нам возможность напрямую ссылаться на компонент и устанавливать локальные параметры.

Далее мы перебираем все передаваемые файлы. Пользователь может выбрать несколько файлов и перетащить их в форму. Нам нужно захватить все эти файлы и перебрать их, добавив файлы в массив локальных файлов. Чтобы получить доступ к файлам, мы ссылаемся на e.dataTransfer.files из события, чтобы получить нужные нам файлы. Это то, что мы повторяем и добавляем каждый в массив files. Теперь мы можем использовать их позже для отправки на наш сервер! Каждый файл представлен объектом File: File – Web APIs | MDN.

Это база для того, что нам нужно что бы работало перетаскивание файлов!

Добавим превью выбранных файлов (если выбранные файлы являются изображениями)

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

Отображение файлов после перетаскивания

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

В нашем шаблоне прямо под формой добавьте следующую разметку:

<div v-for="(file, key) in files" class="file-listing">
      <img class="preview" v-bind:ref="'preview'+parseInt( key )"/>
      {{ file.name }}
</div>

То, что это делает?…, это перебирает все файлы в массиве files, создавая простой шаблон предварительного просмотра. В нашем шаблоне первый элемент — это тег <img>, который связывает ссылку на предварительный просмотр и ключ файла в массиве files. Это так же поможет установить атрибут src через код, если это изображение. Мы сделаем это на следующем этапе.

Другая часть шаблона — это просто имя файла, предоставленного пользователем.

Добавим немного стилей (так что все это не было в полном беспорядке), я добавил следующий CSS к тегу <style> в компоненте:

div.file-listing{
  width: 400px;
  margin: auto;
  padding: 10px;
  border-bottom: 1px solid #ddd;
}

div.file-listing img{
  height: 100px;
}

Реализация предварительного просмотра

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

Первая строка кода, которую мы добавим, находится в обработчике события drop. При запуске после добавления файлов в локальный массив files нам нужно добавить этот код:

this.getImagePreviews();

Этот метод отобразит превью изображения или изображение заполнителя, если тип файла не является изображением.

Теперь добавим этот метод в массив methods нашего компонента:

getImagePreviews(){
  for( let i = 0; i < this.files.length; i++ ){
    if ( /\.(jpe?g|png|gif)$/i.test( this.files[i].name ) ) {
      let reader = new FileReader();

      reader.addEventListener("load", function(){
        this.$refs['preview'+parseInt( i )][0].src = reader.result;
      }.bind(this), false);

      reader.readAsDataURL( this.files[i] );
    } else {

      this.$nextTick(function(){
        this.$refs['preview' + parseInt( i )][0].src = '/images/file.png';
      });
    }
  }
}

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

Далее мы проверяем, является ли тип файла изображения:

if ( /\.(jpe?g|png|gif)$/i.test( this.files[i].name ) ) {

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

Если файл является изображением, мы инициализируем чтение файлов:

let reader = new FileReader();

Затем мы привязываем событие load к event, чтобы позволить нам подключить и отобразить изображение, как только оно будет считано из файла, загруженного пользователем:

reader.addEventListener("load", function(){
  this.$refs['preview'+parseInt( i )][0].src = reader.result;
}.bind(this), false);

Мы используем .bind (this) для прослушивателя событий, для того чтобы получить доступ к нашим глобальным $refs и установить src тега img для результата читателя, который является большим двоичным объектом изображения.

Мы ссылаемся на $ref со значением i, чтобы привязать src к правому тегу изображения. Когда мы генерируем наш шаблон, мы основываем атрибут ref на индексе файлов в массиве files.

Наконец, мы считываем изображение:

reader.readAsDataURL( this.files[i] );

Это переносит изображение в массиве файлов, у которого есть индекс i. Когда файл был загружен (прочитан), наше событие заполняет файл и устанавливает src тега image в то, что было прочитано.

Теперь для других типов файлов мы хотим использовать простое изображение-заполнитель. Мы делаем это с помощью оператора else после проверки изображения.

Поскольку мы ограничены областью действия файла как изображения, мы знаем, что это файл другого типа. Надо сказать, что мы должны работать внутри метода $nextTick в VueJS. Поскольку мы выполняем итерации по файлам, а VueJS отображает шаблон, мы должны убедиться, что ссылки связаны, так чтобы мы могли установить соответствующий атрибут в теге src:

this.$nextTick(function(){
  this.$refs['preview'+parseInt( i )][0].src = '/images/file.png';
});

Это все, что нам нужно сделать, чтобы обеспечить простой предварительный просмотр загруженных файлов.

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

Разрешаем пользователям удалять файлы, которые им не нужны

Это одна из наиболее важных функций при загрузке нескольких файлов, которая позволяет пользователям удалить файлы, которые им не нужны. Мы будем строить это подобно тому, что мы создали в нашем первом уроке: Загрузка файлов с помощью VueJS и Axios.

Настройте шаблона

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

<div class="remove-container">
  <a class="remove" v-on:click="removeFile( key )">Remove</a>
</div>

При клике мы вызываем метод remove( key ).

Реализация метода removeFile(key)

Метод removeFile(key) очень прост в реализации. Просто добавьте следующий код в объект methods компонента:

removeFile( key ){
  this.files.splice( key, 1 );
}

После удаления файла VueJS повторно отобразит DOM, и файл не будет отображаться. Это все, что нужно!

Несмотря на то, что в этом руководстве мы не уделяем большого внимания CSS, я добавил следующее для функции удаления, чтобы было легче нажимать на кнопку удаления:

div.remove-container{
  text-align: center;
}

div.remove-container a{
  color: red;
  cursor: pointer;
}

Затем мы фактически делаем наш запрос на отправку файлов на сервер.

Загрузка выбранных файлов

На этом этапе мы фактически отправим запрос с файлами на сервер. Это было подробно описано в нашей первой статье: Загрузка файлов с помощью VueJS и Axios. Мы будем использовать класс FormData(), предоставленный для выполнения этого запроса. Таким образом, мы можем связать нужные нам файлы и отправить их с Axios на сервер.

Кнопка Добавить

Первым шагом является добавление в наш компонент кнопки, которую пользователь может вызвать для загрузки файлов. Для этого добавьте следующую разметку в шаблон после отображения выбранных файлов:

<a class="submit-button" v-on:click="submitFiles()" v-show="files.length > 0">Submit</a>

Первый атрибут в шаблоне, который мы должны отметить, это атрибут v-on:click=»submitFiles()».

Далее следует отметить атрибут v-show=»files.length > 0″. Мы будем показывает кнопку Submit, только если есть загруженные файлы. Это простая вещь UX, но она должна быть удобной для пользователя.

Я также добавил несколько стилей для кнопки:

a.submit-button{
  display: block;
  margin: auto;
  text-align: center;
  width: 200px;
  padding: 10px;
  text-transform: uppercase;
  background-color: #CCC;
  color: white;
  font-weight: bold;
  margin-top: 20px;
}

Обработчик submitFiles()

В объекте methods компонента добавьте следующий метод:

submitFiles(){
  
  let formData = new FormData();

  for( var i = 0; i < this.files.length; i++ ){
    let file = this.files[i];

    formData.append('files[' + i + ']', file);
  }

  axios.post( '/file-drag-drop',
    formData,
    {
      headers: {
          'Content-Type': 'multipart/form-data'
      }
    }
  ).then(function(){
    console.log('SUCCESS!!');
  })
  .catch(function(){
    console.log('FAILURE!!');
  });
},

Тут мы сначала инициализируем объект FormData():

let formData = new FormData();

Это позволит нам сформировать наш запрос для отправки на сервер.

Далее, мы добавляем каждый из файлов, находящихся в массиве files, в FormData():

for( var i = 0; i < this.files.length; i++ ){
  let file = this.files[i];

  formData.append('files[' + i + ']', file);
}

Далее мы делаем запрос Axios POST:

axios.post( '/file-drag-drop',
  formData,
  {
    headers: {
        'Content-Type': 'multipart/form-data'
    }
  }
).then(function(){
  console.log('SUCCESS!!');
})
.catch(function(){
  console.log('FAILURE!!');
});

Первый параметр в запросе — это URL, на который мы отправляем файлы. Это просто конечная точка, которую я настроил на тестовом сервере для приема запроса файла. Второй параметр — это formData, которая содержит все выбранные файлы. Это также может содержать любые другие входные данные, которые необходимо отправить вместе с файлами, такими как текстовые поля, логические значения и т. д.

Третий параметр является наиболее важным в этом случае. Он позволяет нам настроить наш запрос и добавить дополнительные заголовки. Чтобы отправить файлы на сервер, нам нужно добавить заголовок ‘Content-Type’: ‘multipart/form-data’, чтобы сервер знал, что нужно принимать файлы в случае необходимости.

Наконец, мы подключаемся к результату успешного запроса через добавление .then(function () {}) к запросу и результату неудачного запроса с помощью .catch(function () {}). Они могут быть использованы для отображения сообщений для ваших пользователей. В нашем случае я просто вывожу сообщение в консоль для тестирования.

Это все, что нам нужно для запроса. Далее, мы добавим индикатор загрузки. Это немного улучшит наш UX.

Добавление индикатора загрузки

Это последний шаг перед тем, как мы отправим файлы на сервер. Сначала добавим разметку в шаблон компонента Vue для обработки индикатора выполнения:

<progress max="100" :value.prop="uploadPercentage"></progress>

Подробное описание процесса создания индикатора прогресса: Индикатор хода загрузки файла с Axios и VueJS. Здесь я кратко расскажу о функциях, на которые вам нужно обратить внимание.

Первый атрибут, на который мы должны обратить внимание, это атрибут max. Он должен быть установлено в 100. Это означает, что прогресс будет между 0 и 100, так как мы хотим представить загрузку файлов в процентах.

Второй атрибут — это значение value. Таким образом, мы можем сделать так, чтобы VueJS связывал реактивное свойство с элементом, так как он будет изменять свое значение в соответствии со статусом загрузки. Значение, которое связано с этим свойством, мы установим на следующем шаге.

Так же я добавил немного стилей к тегу styles, чтобы на данный момент индикатор стал немного более удобным:

progress{
  width: 400px;
  margin: auto;
  display: block;
  margin-top: 20px;
  margin-bottom: 20px;
}

Добавим data в шаблон

Теперь, когда мы установили шаблон, давайте добавим значение uploadPercentage к методу data() в компоненте следующим образом:

data(){
  return {
    dragAndDropCapable: false,
    files: [],
    uploadPercentage: 0
  }
},

Мы инициализируем его значение равным 0, так что процентное отношение будет увеличиваться по мере загрузки.

Добавить listener в запрос на загрузку файла

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

Чтобы добавить этот listener, добавьте следующий код в конфигурацию запроса (третий параметр):

onUploadProgress: function( progressEvent ) {
  this.uploadPercentage = parseInt( Math.round( ( progressEvent.loaded * 100 ) / progressEvent.total ) );
}.bind(this)

Что это делает ?… , это слушает событие прогресса загрузки и устанавливает значение переменной uploadProgress. Событие onUploadProgress возвращает, сколько было загружено (отправлено) и сколько нужно отправить.

Это все, что нам нужно сделать для загрузки файлов с помощью перетаскивания. Далее мы изменим наш компонент перетаскивания, чтобы сделать прямую загрузку без кнопки отправки.

На данный момент наш компонент должен выглядеть следующим образом:

<style>
  form{
    display: block;
    height: 400px;
    width: 400px;
    background: #ccc;
    margin: auto;
    margin-top: 40px;
    text-align: center;
    line-height: 400px;
    border-radius: 4px;
  }

  div.file-listing{
    width: 400px;
    margin: auto;
    padding: 10px;
    border-bottom: 1px solid #ddd;
  }

  div.file-listing img{
    height: 100px;
  }

  div.remove-container{
    text-align: center;
  }

  div.remove-container a{
    color: red;
    cursor: pointer;
  }

  a.submit-button{
    display: block;
    margin: auto;
    text-align: center;
    width: 200px;
    padding: 10px;
    text-transform: uppercase;
    background-color: #CCC;
    color: white;
    font-weight: bold;
    margin-top: 20px;
  }

  progress{
    width: 400px;
    margin: auto;
    display: block;
    margin-top: 20px;
    margin-bottom: 20px;
  }
</style>

<template>
  <div id="file-drag-drop">
    <form ref="fileform">
      <span class="drop-files">Drop the files here!</span>
    </form>

    <progress max="100" :value.prop="uploadPercentage"></progress>

    <div v-for="(file, key) in files" class="file-listing">
      <img class="preview" v-bind:ref="'preview'+parseInt( key )"/>
      {{ file.name }}
      <div class="remove-container">
        <a class="remove" v-on:click="removeFile( key )">Remove</a>
      </div>
    </div>

    <a class="submit-button" v-on:click="submitFiles()" v-show="files.length > 0">Submit</a>
  </div>
</template>

<script>
  export default {
    data(){
      return {
        dragAndDropCapable: false,
        files: [],
        uploadPercentage: 0
      }
    },

    mounted(){
      this.dragAndDropCapable = this.determineDragAndDropCapable();

      if( this.dragAndDropCapable ){

        ['drag', 'dragstart', 'dragend', 'dragover', 'dragenter', 'dragleave', 'drop'].forEach( function( evt ) {

          this.$refs.fileform.addEventListener(evt, function(e){
            e.preventDefault();
            e.stopPropagation();
          }.bind(this), false);
        }.bind(this));

        this.$refs.fileform.addEventListener('drop', function(e){
          for( let i = 0; i < e.dataTransfer.files.length; i++ ){
            this.files.push( e.dataTransfer.files[i] );
            this.getImagePreviews();
          }
        }.bind(this));
      }
    },

    methods: {
      determineDragAndDropCapable(){

        var div = document.createElement('div');

        return ( ( 'draggable' in div )
                || ( 'ondragstart' in div && 'ondrop' in div ) )
                && 'FormData' in window
                && 'FileReader' in window;
      },

      getImagePreviews(){
        for( let i = 0; i < this.files.length; i++ ){
          if ( /\.(jpe?g|png|gif)$/i.test( this.files[i].name ) ) {

            let reader = new FileReader();

            reader.addEventListener("load", function(){
              this.$refs['preview'+parseInt( i )][0].src = reader.result;
            }.bind(this), false);

            reader.readAsDataURL( this.files[i] );
          } else {
            this.$nextTick(function(){
              this.$refs['preview'+parseInt( i )][0].src = '/images/file.png';
            });
          }
        }
      },

      submitFiles(){

        let formData = new FormData();

        for( var i = 0; i < this.files.length; i++ ){
          let file = this.files[i];

          formData.append('files[' + i + ']', file);
        }

        axios.post( '/file-drag-drop',
          formData,
          {
            headers: {
                'Content-Type': 'multipart/form-data'
            },
            onUploadProgress: function( progressEvent ) {
              this.uploadPercentage = parseInt( Math.round( ( progressEvent.loaded * 100 ) / progressEvent.total ) );
            }.bind(this)
          }
        ).then(function(){
          console.log('SUCCESS!!');
        })
        .catch(function(){
          console.log('FAILURE!!');
        });
      },

      removeFile( key ){
        this.files.splice( key, 1 );
      }
    }
  }
</script>

Прямая загрузка без кнопки «Submit»

Это немного другой пользовательский интерфейс, который может соответствовать потребностям некоторых пользователей. Вместо того, чтобы перетаскивать файлы, а затем проверять выбранные файлы и нажимать кнопку «Submit», можно сразу загружать все файлы, когда они помещаются в форму.

Для этого случая я быстро создал новый компонент Vue с именем FileDragDropInstant.vue и добавил функциональность, которую мы использовали из нашей основы перетаскивания. Это даст нам большую часть необходимой нам функциональности.

Наш компонент FileDragDropInstant.vue должен начинаться так:

<style>
  form{
    display: block;
    height: 400px;
    width: 400px;
    background: #ccc;
    margin: auto;
    margin-top: 40px;
    text-align: center;
    line-height: 400px;
    border-radius: 4px;
  }
</style>

<template>
  <div id="file-drag-drop">
    <form ref="fileform">
      <span class="drop-files">Drop the files here!</span>
    </form>
  </div>
</template>

<script>
  export default {
    data(){
      return {
        dragAndDropCapable: false,
        files: []
      }
    },

    mounted(){
      this.dragAndDropCapable = this.determineDragAndDropCapable();

      if( this.dragAndDropCapable ){
        ['drag', 'dragstart', 'dragend', 'dragover', 'dragenter', 'dragleave', 'drop'].forEach( function( evt ) {

          this.$refs.fileform.addEventListener(evt, function(e){
            e.preventDefault();
            e.stopPropagation();
          }.bind(this), false);
        }.bind(this));

        this.$refs.fileform.addEventListener('drop', function(e){
          for( let i = 0; i < e.dataTransfer.files.length; i++ ){
            this.files.push( e.dataTransfer.files[i] );
          }
        }.bind(this));
      }
    },

    methods: {
      determineDragAndDropCapable(){

        var div = document.createElement('div');

        return ( ( 'draggable' in div )
                || ( 'ondragstart' in div && 'ondrop' in div ) )
                && 'FormData' in window
                && 'FileReader' in window;
      }
    }
  }
</script>

На данный момент он просто предоставляет форму, куда мы можем перетащить файл поверх формы. Мы также проверяем, поддерживает ли форма перетаскивание.

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

Добавим метод submitFiles()

Этот метод является тем же методом, который мы использовали в другом компоненте загрузки с перетаскиванием. Он просто передает форму с файлами на сервер. Добавьте следующий метод к объекту methods:

submitFiles(){
  let formData = new FormData();

  for( var i = 0; i < this.files.length; i++ ){
    let file = this.files[i];

    formData.append('files[' + i + ']', file);
  }

  axios.post( '/file-drag-drop-instant',
    formData,
    {
      headers: {
          'Content-Type': 'multipart/form-data'
      },
      onUploadProgress: function( progressEvent ) {
        this.uploadPercentage = parseInt( Math.round( ( progressEvent.loaded * 100 ) / progressEvent.total ) );
      }.bind(this)
    }
  ).then(function(){
    console.log('SUCCESS!!');
  })
  .catch(function(){
    console.log('FAILURE!!');
  });
}

Нам также необходимо добавить переменную uploadPercentage в наш метод data() и добавить индикатор выполнения обратно в наш шаблон. Теперь наш компонент должен выглядеть так:

<style>
  form{
    display: block;
    height: 400px;
    width: 400px;
    background: #ccc;
    margin: auto;
    margin-top: 40px;
    text-align: center;
    line-height: 400px;
    border-radius: 4px;
  }

  progress{
    width: 400px;
    margin: auto;
    display: block;
    margin-top: 20px;
    margin-bottom: 20px;
  }
</style>

<template>
  <div id="file-drag-drop">
    <form ref="fileform">
      <span class="drop-files">Drop the files here!</span>

      <progress max="100" :value.prop="uploadPercentage"></progress>
    </form>
  </div>
</template>

<script>
  export default {
    data(){
      return {
        dragAndDropCapable: false,
        files: [],
        uploadPercentage: 0
      }
    },

    mounted(){
      this.dragAndDropCapable = this.determineDragAndDropCapable();

      if( this.dragAndDropCapable ){
        ['drag', 'dragstart', 'dragend', 'dragover', 'dragenter', 'dragleave', 'drop'].forEach( function( evt ) {
          this.$refs.fileform.addEventListener(evt, function(e){
            e.preventDefault();
            e.stopPropagation();
          }.bind(this), false);
        }.bind(this));

        this.$refs.fileform.addEventListener('drop', function(e){
          for( let i = 0; i < e.dataTransfer.files.length; i++ ){
            this.files.push( e.dataTransfer.files[i] );
          }
        }.bind(this));
      }
    },

    methods: {
      determineDragAndDropCapable(){
        var div = document.createElement('div');

        return ( ( 'draggable' in div )
                || ( 'ondragstart' in div && 'ondrop' in div ) )
                && 'FormData' in window
                && 'FileReader' in window;
      },

      submitFiles(){
        let formData = new FormData();

        for( var i = 0; i < this.files.length; i++ ){
          let file = this.files[i];

          formData.append('files[' + i + ']', file);
        }

        axios.post( '/file-drag-drop-instant',
          formData,
          {
            headers: {
                'Content-Type': 'multipart/form-data'
            },
            onUploadProgress: function( progressEvent ) {
              this.uploadPercentage = parseInt( Math.round( ( progressEvent.loaded * 100 ) / progressEvent.total ) );
            }.bind(this)
          }
        ).then(function(){
          console.log('SUCCESS!!');
        })
        .catch(function(){
          console.log('FAILURE!!');
        });
      }
    }
  }
</script>

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

Связывая все вместе

Хитрость заключается в том, чтобы просто, вызвав наш метод submitFiles() сразу после того, как пользователь поместит файлы в форму.

В нашем обработчике события drop в хуке жизненного цикла mount() нам просто нужно добавить вызов метода submitFiles().

Наш обработчик должен выглядеть так:

this.$refs.fileform.addEventListener('drop', function(e){
  for( let i = 0; i < e.dataTransfer.files.length; i++ ){
    this.files.push( e.dataTransfer.files[i] );
  }
  this.submitFiles();
}.bind(this));

Теперь, когда пользователь помещает файл в форму, он немедленно отправляется на сервер.

Заключение

Это была заключительная статья из серии статей посвященных реализации компонента загрузки файлов, с необходимым функционалом, на базе VueJs и Axios. Если у вас остались вопросы, не стесняйтесь пишите мне (в блог автора оригинальной статьи) .

Удачи в реализации загрузки файлов на сервер!

Оригинальная статья: Dan Pastori — Drag & Drop File Uploads with VueJS and Axios

Была ли вам полезна эта статья?
[7 / 4.4]

Spread the love
Подписаться
Уведомление о
guest
1 Комментарий
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Dmitry
Dmitry
3 лет назад

Спасибо большое за статью, очень полезная