Все течет, все меняется, но только input[type=file]
как портил нервы всем начинающим веб-разработчикам, так и продолжает это делать до сих пор. Вспомните себя N лет назад, когда вы только начинали постигать азы создания веб-сайтов. Молодой и неопытный, вы искренне удивлялись, когда кнопка выбора файла напрочь отказывалась менять цвет своего фона на ваш любимый персиковый. Именно в тот момент вы впервые столкнулись с этим несокрушимым айсбергом под названием «Загрузка файлов», который и по сей день продолжает «топить» начинающих веб-разработчиков.
На примере создания поля для загрузки файлов я покажу вам, как правильно прятать input[type=file]
, настраивать фокус на объекте, у которого фокуса быть не может, обрабатывать события Drag-and-Drop и отправлять файлы через AJAX. А также я познакомлю вас с парой браузерных багов и путями их обхода. Статья написана для новичков, но в некоторых моментах может быть полезна и занимательна даже для матерых разработчиков.
<!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>
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]
убран из разметки):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
, то диалоговое окно откроется и все будет работать как надо, вот только как мы поймем, что нажимать уже пора? :focus
, который определяет стили для элементов в фокусе, и селекторах +
или ~
, которые выбирают правых соседей: элементы, расположенные на том же уровне вложенности, идущие после выбранного элемента. Если учесть, что в нашей разметке input[type=file]
расположен прямо перед тэгом label
, имеет место быть следующая запись:#upload-container input[type=file]:focus + label {
/*Стили для метки*/
}
outline
, которое создает вокруг элемента обводку, отличающуюся от border
тем, что не изменяет размер элемента и может быть отодвинута от него. Как правило, люди пользуются только одним браузером, поэтому привыкают именно к его стандартам. Чтобы людям было проще ориентироваться на нашем сайте, мы должны постараться настроить фокус так, чтобы он выглядел максимально естественно для большинства популярных современных браузеров. В теории, с помощью JavaScript можно получить информацию о том, через какой браузер пользователь открыл сайт, и в соответствии с этим настроить стили, но в рамках статьи, предназначенной в первую очередь для новичков, эта тема слишком сложна и громоздка. Постараемся обойтись малой кровью.: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;
}
:focus {
outline: 1px solid #0078d7;
}
:focus {
outline: 1px solid #212121;
}
-moz-
со свойством outline
работать не будет. Поэтому нам придется выбирать, какое из этих двух свойств мы выберем. Так как количество пользователей Firefox значительно выше, рациональнее отдать предпочтение именно этому браузеру. Это не значит, что мы лишим пользователей Edge и других браузеров возможности видеть, где сейчас фокус, просто он у них будет выглядеть «неродным». Что ж, приходится идти на жертвы. #upload-container input[type=file]:focus + label {
outline: 1px solid #0078d7;
outline: -webkit-focus-ring-color auto 5px;
}
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, dragstart, dragend, dragover, dragenter, dragleave, drop
. Подробное описание каждого из них вы с легкостью сможете найти в интернете. Мы будем отслеживать только некоторые из них. var dropZone = $('#upload-container');
dropZone
, когда курсор, тянущий файл, будет прямо над ним. Это нужно, чтобы визуально проинформировать пользователя о том, что файл уже можно отпустить.#upload-container.dragover {
background-color: #fafafa;
outline-offset: -17px;
}
dropZone.on('drag dragstart dragend dragover dragenter dragleave drop', function(){
return false;
});
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');
};
});
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
и отправляем его в функцию.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({
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
укажем функцию, которая выполнится, если файлы успешно передадутся на сервер. Ее содержимое зависит от вашей фантазии, я же ограничусь скромным выводом сообщения об успешной загрузке.Вы можете помочь и перевести немного средств на развитие сайта
Без рабочей демки как то пресно(
Добавил возможность скачать финальный билд, билд с проблемой с фокусом и билд с мерцанием!
Теперь добавил возможность пощупать, не бейте!
Я аж на календарь глянул, думал снова 2012-ый наступил…
А что, с 2012го успел наступить невиданный прорыв в формах передачи файлов?
Jquery протух, все используют habr.com/post/150594.
Да ну, этот фреймворк уже такой старый. Статья 2012 года и в ней написано, что браузеры уже 10 лет его поддерживают. Как минимум 22 года. Дайте человеку попользоваться чем-то современным, стильным и молодежным.
Зачем? Ниже вон вообще рекомендуют не заморачиваться о том как это сделано, а привинтить очередной невелосипед неизвестного качества из хуермиллиона строк кода, нпм и плюшками.
Да нет, просто уже существует огромная куча готовых решений. Тот же DropzoneJS — наверное, самый известный. Смысл в 2018-ом году писать свой велосипед, да ещё и на jQuery?
Действительно, тема стара как мир. Но вместе с ней стары и сопутствующие ей проблемы. Не вижу ничего плохого в том, чтобы «переиздавать» руководства, дополняя их чем-то свежим. Например, толкового объяснения, а уж тем более решения тех багов, на которые я указал в этой статье, я в интернете, даже англоязычном, не нашел.
Я бы просто добавил в разметку аттрибут «tabindex»:
И никаких заумных стилей, никакой борьбы с разными браузерами, никаких js-обработчиков…
Только ни пробелом, ни enter'ом на такой label не нажать. Только мышкой.
Попробовал, не помогло, но спасибо за вариант! Добавил возможность скачать билды: попробуйте сами, может я что-то не так делаю.
Ваш «финальный», с tabindex-ом.
Чем не устраивает вариант с tabindex-ом?
Да, я все же немного накосячил, но! Фокус действительно будет появляться, вот только, как заметил monochromer выше, нажиматься метка будет только мышью.
А мы просто используем plupload.js, который может слать файлы кусками, и не выставлять max_upload_size в сотни МБ.
Ваши бы слова, да разработчикам ГИС ЖКХ в уши. Шаблоны по одному грузить — рутина. Встречая разработчиков, думающих об удобстве работы даже в таких, казалось бы, «мелочах» как-то даже вера в будущее просыпается.
Еще есть проблемы в ie9 где нет multiple и когда ajax с файлами таки не отправляется и ты используешь iframe. Еще в ie9 другой объект с файлами, там вроде размера нет и еще что-то, кто помнит напомните. Ох в свое время хлебнул для поддержки ie9 с этим полем загрузки файлов.
Если учитывать относительно современные версии Firefox, то баг можно вылечить с помощью css-псевдокласса
:focus-within
.Сам баг: https://bugzilla.mozilla.org/show_bug.cgi?id=1430196
Писал подобный велосипед:
github.com/paulzi/filestyler/blob/master/README.ru.md
Демо: paulzi.ru/github/filestyler/docs