Поле загрузки файлов, которое мы заслужили

в 10:12, , рубрики: css, file upload, html, input[type=file], javascript, веб-дизайн, Разработка веб-сайтов

Все течет, все меняется, но только input[type=file] как портил нервы всем начинающим веб-разработчикам, так и продолжает это делать до сих пор. Вспомните себя N лет назад, когда вы только начинали постигать азы создания веб-сайтов. Молодой и неопытный, вы искренне удивлялись, когда кнопка выбора файла напрочь отказывалась менять цвет своего фона на ваш любимый персиковый. Именно в тот момент вы впервые столкнулись с этим несокрушимым айсбергом под названием «Загрузка файлов», который и по сей день продолжает «топить» начинающих веб-разработчиков.

На примере создания поля для загрузки файлов я покажу вам, как правильно прятать input[type=file], настраивать фокус на объекте, у которого фокуса быть не может, обрабатывать события Drag-and-Drop и отправлять файлы через AJAX. А также я познакомлю вас с парой браузерных багов и путями их обхода. Статья написана для новичков, но в некоторых моментах может быть полезна и занимательна даже для матерых разработчиков.

Разметка и первичные стили

Начнем с HTML-разметки:

<!DOCTYPE html>
<html lang="en">
<head>
     <meta charset="UTF-8">
     <title>Поле загрузки файлов, которое мы заслужили</title>
     <link rel="stylesheet" href="style.css">
     <script type="text/javascript" src="jquery-3.3.1.min.js"></script>
     <script type="text/javascript" src="script.js"></script>
</head>
<body>
     <form id="upload-container" method="POST" action="send.php">
          <img id="upload-image" src="upload.svg">
          <div>
               <input id="file-input" type="file" name="file" multiple>
               <label for="file-input">Выберите файл</label>
               <span>или перетащите его сюда</span>
          </div>
     </form>
</body>
</html>

Пожалуй, главным элементом, на который стоит обратить внимание, является

<label for="file-input">Выберите файл</label>

Спецификация HTML не позволяет нам накладывать визуальные свойства непосредственно на input[type=file], но мы имеем тэг label, нажатие на который вызывает клик по элементу формы, к которому он привязан. К нашей радости, данный тэг никаких ограничений в стилизации не имеет: мы можем делать с ним все, что захотим.

Вырисовывается план действий: стилизуем метку как нам угодно, а сам input[type=file] прячем с глаз долой. Для начала настроим общие стили страницы:

body {
     padding: 0;
     margin: 0;
     display: flex;
     justify-content: center;
     align-items: center;
     min-height: 100vh;
}

#upload-container {
     display: flex;
     justify-content: center;
     align-items: center;
     flex-direction: column;
     width: 400px;
     height: 400px;
     outline: 2px dashed #5d5d5d;
     outline-offset: -12px;
     background-color: #e0f2f7;
     font-family: 'Segoe UI';
     color: #1f3c44;
}

#upload-container img {
     width: 40%;
     margin-bottom: 20px;
     user-select: none;
}

Теперь стилизуем нашу метку:

#upload-container label {
     font-weight: bold;
}

#upload-container label:hover {
     cursor: pointer;
     text-decoration: underline;
}

То, к чему мы стремимся (input[type=file] убран из разметки):
Поле загрузки файлов, которое мы заслужили - 1
Безусловно, можно было отцентровать метку, добавить фон и границу, получив полноценную кнопку, но наш приоритет — Drag-and-Drop.

Прячем input

Теперь нам нужно спрятать input[type=file]. Первое, что бросается в голову — свойства display: none и visibility: hidden. Но тут не все так просто. На некоторых старых браузерах клик по метке перестанет производить какой-либо эффект. Но это не все. Как известно, невидимые элементы не могут получать фокус, а кто бы что ни говорил, фокус важен, так как для некоторых людей это единственная возможность взаимодействия с сайтом. Так что этот способ нас не устраивает. Пойдем обходным путем:

#upload-container div {
     position: relative;
     z-index: 10;
}

#upload-container input[type=file] {
     width: 0.1px;
     height: 0.1px;
     opacity: 0;
     position: absolute;
     z-index: -10;
}

Абсолютно спозиционируем наш input[type=file] относительно его родительского блока, уменьшим до 0.1px, сделаем прозрачным и установим его z-index меньше, чем у родителя, чтоб, так сказать, наверняка.

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

Настраиваем фокус

Так как наш input[type=file] физически присутствует на страницу, он имеет возможность получать фокус. То есть, если мы будем нажимать на странице клавишу Tab, то в какой-то момент фокус перейдет на input[type=file]. Но проблема в том, что мы этого не увидим: выделяться будет поле, которое мы скрыли. Да, если в этот момент мы нажмем Enter, то диалоговое окно откроется и все будет работать как надо, вот только как мы поймем, что нажимать уже пора?

Наша задача — определенным образом выделить метку в момент, когда фокус расположен на поле загрузки файлов. Но как нам это сделать, если метка получать фокус не может? Знатоки CSS3 сразу же подумают о псевдоклассе :focus, который определяет стили для элементов в фокусе, и селекторах + или ~, которые выбирают правых соседей: элементы, расположенные на том же уровне вложенности, идущие после выбранного элемента. Если учесть, что в нашей разметке input[type=file] расположен прямо перед тэгом label, имеет место быть следующая запись:

#upload-container input[type=file]:focus + label {
     /*Стили для метки*/
}

​Но опять же, не все так просто. Для начала давайте обсудим, каким образом нам следует выделить метку. Как известно, все современные и не очень браузеры имеют уникальные свойства по умолчанию для элементов в фокусе. В основном, это свойство outline, которое создает вокруг элемента обводку, отличающуюся от border тем, что не изменяет размер элемента и может быть отодвинута от него. Как правило, люди пользуются только одним браузером, поэтому привыкают именно к его стандартам. Чтобы людям было проще ориентироваться на нашем сайте, мы должны постараться настроить фокус так, чтобы он выглядел максимально естественно для большинства популярных современных браузеров. В теории, с помощью JavaScript можно получить информацию о том, через какой браузер пользователь открыл сайт, и в соответствии с этим настроить стили, но в рамках статьи, предназначенной в первую очередь для новичков, эта тема слишком сложна и громоздка. Постараемся обойтись малой кровью.

В браузерах, основанных на движке WebKet (Google Chrome, Operа, Safari), свойство по умолчанию для элементов в фокусе имеет вид:

:focus {
     outline: -webkit-focus-ring-color auto 5px;
}

Здесь -webkit-focus-ring-color — специфичный только для данного движка цвет фокусной обводки. То есть, эта строчка будет работать исключительно в WebKit-браузерах, а это именно то, что нам нужно. Укажем данное свойство для нашей метки:

#upload-container input[type=file]:focus + label {
     outline: -webkit-focus-ring-color auto 5px;
}

Открываем Google Chrome или Opera, смотрим. Все работает как надо:
Поле загрузки файлов, которое мы заслужили - 2
Посмотрим, как обстоят дела с фокусом в Mozilla Firefox и Microsoft Edge. Для этих браузеров свойство по умолчанию имеет вид:

:focus {
     outline: 1px solid #0078d7;
}

и

:focus {
     outline: 1px solid #212121;
}

соответственно.

К сожалению, префикс -moz- со свойством outline работать не будет. Поэтому нам придется выбирать, какое из этих двух свойств мы выберем. Так как количество пользователей Firefox значительно выше, рациональнее отдать предпочтение именно этому браузеру. Это не значит, что мы лишим пользователей Edge и других браузеров возможности видеть, где сейчас фокус, просто он у них будет выглядеть «неродным». Что ж, приходится идти на жертвы.

Добавляем стиль из Mozilla Firefox перед стилем для WebKit: сначала все браузеры применят первое свойство, а затем те, которые могут (Google Chrome, Opera, Safari и др.), применят второе.

#upload-container input[type=file]:focus + label {	
     outline: 1px solid #0078d7;
     outline: -webkit-focus-ring-color auto 5px;
}

И вот тут начинается странное: в Edge все работает нормально, а вот Firefox по каким-то неведомым причинам отказывается применять свойства к метке при фокусе на input[type=file]. Причем само событие focus случается — проверил через JavaScript. Более того, если принудительно установить фокус на поле выбора файла через инструменты разработчика, то свойство применится и наша обводка появится! Видимо, это баг самого браузера, но если у кого-то есть идеи, почему такое происходит — пишите в комментариях.

Ну ничего, нормальные герои всегда идут в обход. Как я сказал ранее, событие focus случается, а значит, регулировать свойства мы можем прямиком из JavaScript. Но для этого нам придется поменять логику нашего селектора:

#upload-container label.focus {
     outline: 1px solid #0078d7;
     outline: -webkit-focus-ring-color auto 5px;
}

Опишем класс .focus для нашей метки и будем добавлять его каждый раз, когда input[type=file] получает фокус и убирать, когда теряет.

$('#file-input').focus(function() {
     $('label').addClass('focus');
})
.focusout(function() {
     $('label').removeClass('focus');
});

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

Drag-and-Drop

Работа с Drag-and-Drop осуществляется путем отслеживания специальных браузерных событий: drag, dragstart, dragend, dragover, dragenter, dragleave, drop. Подробное описание каждого из них вы с легкостью сможете найти в интернете. Мы будем отслеживать только некоторые из них.

Для начала определим Drag-and-Drop-элемент:

var dropZone = $('#upload-container');

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

#upload-container.dragover {
     background-color: #fafafa;
     outline-offset: -17px;
}

Теперь перейдем в JS-файл. Для начала, нам необходимо отменить все действия по умолчанию на события Drag-and-Drop. Например, одно из таких событий — открытие кинутого файла браузером. Нам это совершенно не нужно, поэтому пропишем следующие строчки:

dropZone.on('drag dragstart dragend dragover dragenter dragleave drop', function(){
     return false;
});

В jQuery вызов оператора return false эквивалентен вызову сразу двух функций: e.preventDefault() и e.stopPropagation().

Начнем описывать свой собственный обработчик событий. Поступим так же, как делали с фокусом, но на этот раз будем отслеживать события dragenter и dragover для добавления класса и событие dragleave для его удаления:

dropZone.on('dragover dragenter', function() {
     dropZone.addClass('dragover');
});

dropZone.on('dragleave', function(e) {
     dropZone.removeClass('dragover');
});

И опять нас ждет неприятный сюрприз: при движении по dropZone мышью с файлом поле начинает мерцать. Происходит это в Microsoft Edge и WebKit-браузерах. Кстати, большинство этих самых WebKit-браузеров в настоящее время работают на движке Blink (оценили иронию, а?). А вот в Mozilla ничего не мерцает. Видимо, решил исправиться после багов с фокусом.

А происходит это мерцание из-за того, что при наведении курсора на дочерний элемент dropZone, будь то картинка или div с полем выбора файлов и меткой, по какой то причине срабатывает событие dragleave. Нам очевидно, что поле мы не покидаем, а вот браузерам, почему-то, нет, и из-за этого они без зазрения совести убирают класс .focus у dropZone.

И вновь нам придется как-то выкручиваться. Если браузер сам не понимает, что поле мы не покидаем, придется ему помочь. А делать мы это будем через дополнительные условия: вычислим координаты мыши относительно dropZone, а затем проверим, вышел ли курсор за пределы блока. Если вышел, значит убираем стиль:

dropZone.on('dragleave', function(e) {
     let dx = e.pageX - dropZone.offset().left;
     let dy = e.pageY - dropZone.offset().top;
     if ((dx < 0) || (dx > dropZone.width()) || (dy < 0) || (dy > dropZone.height())) {
          dropZone.removeClass('dragover');
     };
});

И все, проблема решена! Вот так выглядит наше поле с файлом внутри:
Поле загрузки файлов, которое мы заслужили - 3

Переходим к обработке самого события drop. Но для начала вспомним, что, помимо Drag-and-Drop, у нас есть input[type=file], и каждый из этих способов независим по своей сути, но должен выполнять одинаковые действия: загружать файлы. Поэтому я предлагаю создать отдельную универсальную для обоих методов функцию, в которую мы будем передавать файлы, а она уже будет решать, что с ними сделать. Назовем ее sendFiles(), но опишем чуть позже. Для начала обработаем событие drop:

dropZone.on('drop', function(e) {
     dropZone.removeClass('dragover');
     let files = e.originalEvent.dataTransfer.files;
     sendFiles(files);
});

Сначала уберем класс .dragover у dropZone. Затем получим массив, содержащий файлы. Если вы используете jQuery, то путь будет e.originalEvent.dataTransfer.files, если пишите на чистом JS, то e.dataTransfer.files. Ну а затем передаем массив в нашу пока еще нереализованную функцию.

Теперь проработаем способ загрузки через input[type=file]:

$('#file-input').change(function() {
     let files = this.files;
     sendFiles(files);
});

Отслеживаем событие change на кнопке выбора файлов, получаем массив через this.files и отправляем его в функцию.

Отправка файлов через AJAX

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

Допустим, мы создаем поле для загрузки фотографий. Мы не хотим, чтобы к нам на сервер попало что-то другое, поэтому определимся с типами файлов: пусть это будут PNG и JPEG. Также стоит регламентировать максимальный размер одной фотографии, которую может отправить пользователь. Ограничимся пятью мегабайтами. Начнем описывать нашу функцию:

function sendFiles(files) {
     let maxFileSize = 5242880;
     let Data = new FormData();
     $(files).each(function(index, file) {
          if ((file.size <= maxFileSize) && ((file.type == 'image/png') || (file.type == 'image/jpeg'))) {
               Data.append('images[]', file);
          }
     });
};

В переменную maxFileSize занесем максимальный размер файла, который будем отправлять на сервер. Функцией FormData() мы создадим новый объект класса FormData, позволяющий формировать наборы пар ключ-значение. Такой объект можно легко отправлять через AJAX. Далее используем jQuery конструкцию .each для массива files, которая применит заданную нами функцию для каждого его элемента. В качестве аргументов в функцию будут передаваться порядковый номер элемента и сам элемент, которые мы будем обрабатывать как index и file соответственно. В самой функции мы проверим файл на соответствие нашим критериям: размер меньше пяти мегабайт, а тип — PNG или JPEG. Если файл проходит проверку, то добавляем его в наш объект FormData путем вызова функции append(). Ключом послужит строка 'photos[]', квадратные скобки на конце которой обозначат, что это массив, в котором может быть несколько объектов. Самим объектом будет file.

Теперь все готово для отправки файлов через AJAX. Добавим в нашу функцию следующие строчки:

$.ajax({
     url: dropZone.attr('action'),
     type: dropZone.attr('method'),
     data: Data,
     contentType: false,
     processData: false,
     success: function(data) {
          alert('Файлы были успешно загружены');
     }
});

В качестве параметров url и type укажем соответственно значения атрибутов action и method у input[type=file]. Передавать через AJAX мы будем объект Data. Параметры contentType: false и processData: false нужны для того, чтобы браузер ненароком не перевел наши файлы в какой-то другой формат. В параметре success укажем функцию, которая выполнится, если файлы успешно передадутся на сервер. Ее содержимое зависит от вашей фантазии, я же ограничусь скромным выводом сообщения об успешной загрузке.

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

На этом все. Спасибо за внимание!

Автор: RadicalChild

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js