Поле завантаження файлів, яке ми заслужили

Все тече, все змінюється, але тільки 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] прибраний з розмітки):

Безумовно, можна було відцентрувати мітку, додати фон і кордон, отримавши повноцінну кнопку, але наш пріоритет — 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.1 px;
 height: 0.1 px;
 opacity: 0;
 position: absolute;
 z-index: -10;
}

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

Вітаю, ми домоглися того, чого хотіли: наше поле виглядає саме так, як на попередньому зображенні.

Налаштовуємо фокус

Так як наш input[type=file] фізично присутній на сторінці він має можливість отримувати фокус. Тобто, якщо ми будемо натискати на сторінці клавішу Tab, то в якийсь момент фокус перейде на input[type=file]. Але проблема в тому, що ми цього не побачимо: виділятися буде поле, яке ми приховали. Так, якщо в цей момент ми натиснемо Enter, то відкриється діалогове вікно і все буде працювати як треба, от тільки як ми зрозуміємо, що натискати вже пора?

Читайте також  10 прийомів по створенню гарних бізнес презентацій з 2017 року

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

#upload-container input[type=file]:focus + label {
 /*Стилі для позначки*/
}

Але знову ж таки, не все так просто. Для початку давайте обговоримо, яким чином нам слід виділити мітку. Як відомо, всі сучасні і не дуже браузери мають унікальні властивості елементів за замовчуванням у фокусі. В основному, це властивість outline, яке створює навколо елемента обведення, що відрізняється від border тим, що не змінює розмір елемента і може бути відсунута від нього. Як правило, люди користуються тільки одним браузером, тому звикають саме до його стандартів. Щоб людям було простіше орієнтуватися на нашому сайті, ми повинні постаратися налаштувати фокус так, щоб він виглядав максимально природно для більшості популярних сучасних браузерів. В теорії, за допомогою JavaScript можна отримати інформацію про те, через який браузер користувач відкрив сайт, і відповідно до цього змінювати стилі, але в рамках статті, призначеної в першу чергу для новачків, ця тема дуже складна і громіздка. Спробуємо обійтися малою кров’ю.

У браузерах, заснованих на движку WebKet (Google Chrome, Орега, 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 або Mozilla, дивимося. Все працює як треба:

Подивимося, як йдуть справи з фокусом в Mozilla Firefox і Microsoft Edge. Для цих браузерів властивість за замовчуванням має вигляд:

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

і

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

відповідно.

На жаль, префікс -moz- з властивістю outline працювати не буде. Тому нам доведеться вибирати, яке з цих двох властивостей ми виберемо. Так як кількість користувачів Firefox значно вище, раціональніше віддати перевагу саме цьому браузеру. Це не означає, що ми позбавимо користувачів Edge і інших браузерів можливості бачити, де зараз фокус, просто він у них буде виглядати «нерідною». Що ж, доводиться йти на жертви.

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

#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. Більш того, якщо примусово встановити фокус на поле вибору файлу через інструменти розробника, то властивість застосується і наша обведення з’явиться! Мабуть, це баг самого браузера, але якщо у когось є ідеї, чому таке відбувається — пишіть у коментарях.

Читайте також  Робимо 3D конфігуратор без програмування і верстки. Частина друга

Ну нічого, нормальні герої завжди йдуть в обхід. Як я сказав раніше, подія 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');
};
});

І все, проблема вирішена! Ось так виглядає наше поле з файлом всередині:

Переходимо до обробки самої події 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. Ну а потім передаємо масив в нашу поки ще нереалізовану функцію.

Читайте також  10 корисних порад щодо реалізації Pixel Perfect дизайну під Front-end розробці (на прикладі роботи з редактором Sketch)

Тепер опрацюємо спосіб завантаження через 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 вкажемо функцію, яка виконається, якщо файли успішно передаються на сервер. Її вміст залежить від вашої фантазії, я ж обмежуся скромним виведенням повідомлення про успішне завантаження.

Вітаю, тепер ви вмієте створювати своє власне поле завантаження файлів! Звичайно ж, я не позиціоную свій спосіб як єдино вірний і правильний. Своїм завданням я ставив показати загальний хід вирішення даної задачі, підходить в першу чергу для новачків. Якщо ви вважаєте, що десь щось можна було зробити краще — пишіть в коментарях, обговоримо!

На цьому все. Спасибі за увагу!

Степан Лютий

Обожнюю технології в сучасному світі. Хоча частенько і замислююся над тим, як далеко вони нас заведуть. Не те, щоб я прям і знаюся на ядрах, пікселях, коллайдерах і інших парсеках. Просто приходжу в захват від того, що може в творчому пориві вигадати людський розум.

You may also like...

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *