Простая drag-n-drop зона для отправки файлов с помощью FormIt
Привет, друзья!
Передо мной возникла казалось бы, банальная задача — сделать форму, которая будет отправлять файлы на почту с drag-n-drop зоной.
Мне почему-то крайне не хотелось подключать и развлекаться со сторонними библиотеками типа dropzone.js или filepond, да и вообще как-то не очень много информации я нашел на этот счёт, поэтому было решено сделать своё небольшое решение, как говорится, на коленке, которым я с вами и поделюсь. Моё решение представляет из себя простую визуальную дроп-зону, она не загружает файлы на сервер и т.д., то есть вы просто скидываете в неё несколько файлов, а их отправка на почту будет производиться средствами FormIt.
Итак, у нас есть обычный вызов формы:
Чанк формы form_with_file_tpl:
Обратите внимание, что у формы должен быть класс .form-with-files (по этому классу мы привязываем js), а также атрибут enctype=«multipart/form-data» для корректной отправки файлов.
Добавляем немного css:
Теперь нам понадобится js. Сам код я частично брал из прошлых наработок, поэтому он на jquery, но я думаю кому нужно, смогут переписать его и на ванильный js.
В качестве уведомлений здесь используется библиотека sweetalert, опять же, можно поменять на любую другую.
Делать проверки на js хорошо для пользователя, но нам всё же необходимо добавить проверку со стороны сервера. Для этого нам понадобится небольшой хук, я назвал его checkAttachedFiles. Этот хук обязательно нужно вставить в вызове Formit в hooks в самое начало.
Вот и всё. Как это будет работать можно увидеть в этом коротком видео:

Особенностью данного решения является то, что на странице может быть сколько угодно форм с drag-n-drop зоной и все они будут работать независимо друг от друга. Ну должны во всяком случае)
Если я где-то допустил ошибку — прошу поправить. Надеюсь это будет кому-то полезно.
Благодарю за внимание!
Передо мной возникла казалось бы, банальная задача — сделать форму, которая будет отправлять файлы на почту с drag-n-drop зоной.
Мне почему-то крайне не хотелось подключать и развлекаться со сторонними библиотеками типа dropzone.js или filepond, да и вообще как-то не очень много информации я нашел на этот счёт, поэтому было решено сделать своё небольшое решение, как говорится, на коленке, которым я с вами и поделюсь. Моё решение представляет из себя простую визуальную дроп-зону, она не загружает файлы на сервер и т.д., то есть вы просто скидываете в неё несколько файлов, а их отправка на почту будет производиться средствами FormIt.
{'!AjaxForm' | snippet : [
'snippet' => 'FormIt',
'hooks' => 'checkAttachedFiles,email',
'form' => 'form_with_file_tpl',
'emailFrom' => 'email_from' | config,
'emailFromName' => 'site_name' | config,
'emailSubject' => 'Заявка с сайта ' ~ 'site_name' | config,
'emailTo' => 'email_to' | config,
'emailTpl' => 'modal_form_mail',
'formFields' => 'phone,mail,file',
'validate' => 'mail:email:required,phone:required:regexp=^/\+7 \(9\d{2}\) \d{3}-\d{2}-\d{2}/^',
'successMessage' => 'Ваша заявка успешно отправлена!',
'validationErrorMessage' => 'В форме содержатся ошибки!'
]}
Чанк формы form_with_file_tpl:
<form action="" method="post" class="ajax_form form-with-files" enctype="multipart/form-data">
<div class="form-group">
<input
class="form-control"
type="tel"
name="phone"
placeholder="Ваш контактный телефон: *"
value="{$_modx->getPlaceholder('fi.phone')}"
id="af_phone"
autocomplete="off">
<span class="error_phone">{$_modx->getPlaceholder('fi.error.phone')}</span>
</div>
<div class="form-group">
<input
class="form-control"
type="email"
name="mail"
placeholder="Контактный Email: *"
value="{$_modx->getPlaceholder('fi.mail')}"
id="af_mail"
autocomplete="off">
<span class="error_mail">{$_modx->getPlaceholder('fi.error.mail')}</span>
</div>
<div class="form-group">
<div class="drop-zone">
<div class="drop-message">
<p>Выберите файл или переместите его в поле
<span>Допускаются изображения или doc, docx, xls, xlsx, pdf, txt. Макс. 5 файлов до 10 Мб.</span></p>
<img class="input__file-icon" src="assets/images/attach.svg" alt="Выбрать файл" width="32" height="32">
</div>
<input
class="drop-input form-control-file first"
name="file[]"
type="file"
class="input input__file"
autocomplete="off"
multiple>
</div>
<span class="error_file">{$_modx->getPlaceholder('fi.error.file')}</span>
<ul class="added-files"></ul>
</div>
<button class="btn btn-purple" type="submit">Отправить</button>
</form>
Обратите внимание, что у формы должен быть класс .form-with-files (по этому классу мы привязываем js), а также атрибут enctype=«multipart/form-data» для корректной отправки файлов.
Добавляем немного css:
.drop-zone {
background-color: rgba(20, 20, 38, 0.94);
position: relative;
}
.drop-input {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: block;
opacity: 0;
max-height: 60px;
cursor: pointer;
}
.drop-message {
display: flex;
column-gap: 30px;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
height: 60px;
max-height: 60px;
color: #fff;
}
.drop-message p {
margin-bottom: 0;
}
.drop-message p span {
font-size: 10px;
}
.added-files {
font-size: 12px;
width: 100%;
margin-bottom: 0;
margin-top: 10px;
}
Теперь нам понадобится js. Сам код я частично брал из прошлых наработок, поэтому он на jquery, но я думаю кому нужно, смогут переписать его и на ванильный js.
$(function(){
if (window.File && window.FileReader && window.FileList) {
$('.form-with-files').each(function() {
let form = $(this);
form.find('.drop-zone').on('dragover', function(e) {
$(this).css({
'border': '2px dashed #fff',
'background-color': 'rgba(73, 69, 117, 1)'
});
});
// Обработка dragenter для всей страницы
$(document).on('dragenter', function(e) {
form.find('.drop-zone').css({
'border': '2px dashed #fff',
'background-color': 'rgba(73, 69, 117, 0.85)'
});
});
let originalDropInput = form.find('.drop-input').first(); // Оригинальный input
let dropInput = originalDropInput.clone().addClass('cloned').removeClass('first'); // Клон для использования
// Функция для обработки выбора файлов
function handleFileSelect(evt) {
let files = evt.target.files;
let currentFileCount = form.find('.file-name').length;
let newFileCount = files.length;
// Очищаем список файлов перед добавлением новых
form.find('.added-files').empty();
// Удаляем все клонированные .drop-input и возвращаем первый
form.find('.drop-input.cloned').remove();
form.find('.drop-input.first').css('display', 'block');
if (currentFileCount + newFileCount > 5) {
Swal.fire({
position: "top-end",
icon: "error",
title: "Максимальное количество файлов — 5!",
showConfirmButton: false,
timer: 3000,
toast: true,
width: "19rem"
});
return; // Прекращаем выполнение функции
}
for (var i = 0, f; f = files[i]; i++) {
if (!f.type.match('image.*') && !/\.(doc|docx|xls|xlsx|pdf|txt)$/i.test(f.name)) {
Swal.fire({
position: "top-end",
icon: "error",
title: "Недопустимый тип файла!",
showConfirmButton: false,
timer: 3000,
toast: true,
width: "19rem"
});
continue;
}
if (f.size > 10 * 1024 * 1024) {
Swal.fire({
position: "top-end",
icon: "error",
title: "Недопустимый размер файла!",
showConfirmButton: false,
timer: 3000,
toast: true,
width: "19rem"
});
continue;
}
const reader = new FileReader();
reader.onload = (function(theFile) {
return function(e) {
// Добавляем имя файла в список
$('<li class="file-name">' + theFile.name + '</li>').appendTo(form.find('.added-files'));
// Клонируем .drop-input и добавляем его в .drop-zone
let temp_input = dropInput.clone();
form.find('.drop-input').hide();
form.find('.drop-zone').append(temp_input);
form.find('.drop-zone').css('background-color', 'rgba(20, 20, 38, 0.94)');
temp_input.show();
};
})(f);
// Читаем файл как Data URL
reader.readAsDataURL(f);
}
}
// Вешаем обработчик на .drop-input внутри текущей формы
form.on('change', '.drop-input', handleFileSelect);
});
}
});
В качестве уведомлений здесь используется библиотека sweetalert, опять же, можно поменять на любую другую.
Делать проверки на js хорошо для пользователя, но нам всё же необходимо добавить проверку со стороны сервера. Для этого нам понадобится небольшой хук, я назвал его checkAttachedFiles. Этот хук обязательно нужно вставить в вызове Formit в hooks в самое начало.
<?php
$maxFileSize = 10 * 1024 * 1024; // Максимальный размер файла 10 МБ
$maxFiles = 5; // Максимум 5 файлов
$allowedMimeTypes = [
'image/jpeg', 'image/png', 'image/gif', 'image/webp', // изображения
'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .doc, .docx
'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xls, .xlsx
'application/pdf', 'text/plain' // .pdf, .txt
];
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'doc', 'docx', 'xls', 'xlsx', 'pdf', 'txt'];
// Если файлов нет — пропускаем проверку
if (empty($_FILES['file']['name'][0])) {
return true;
}
// Проверка количества файлов
if (count($_FILES['file']['name']) > $maxFiles) {
$hook->addError('file', "Можно загрузить не более $maxFiles файлов.");
return false;
}
foreach ($_FILES['file']['name'] as $key => $fileName) {
$fileTmpName = $_FILES['file']['tmp_name'][$key];
$fileType = mime_content_type($fileTmpName);
$fileSize = $_FILES['file']['size'][$key];
$fileExt = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
$fileError = $_FILES['file']['error'][$key];
if ($fileError !== UPLOAD_ERR_OK) {
$hook->addError('file', "Ошибка загрузки файла: $fileName.");
return false;
}
if (!in_array($fileType, $allowedMimeTypes)) {
$hook->addError('file', "Недопустимый тип файла: $fileName.");
return false;
}
if (!in_array($fileExt, $allowedExtensions)) {
$hook->addError('file', "Недопустимое расширение файла: $fileName.");
return false;
}
if ($fileSize > $maxFileSize) {
$hook->addError('file', "Файл $fileName превышает допустимый размер 10 МБ.");
return false;
}
}
return true;
Вот и всё. Как это будет работать можно увидеть в этом коротком видео:

Особенностью данного решения является то, что на странице может быть сколько угодно форм с drag-n-drop зоной и все они будут работать независимо друг от друга. Ну должны во всяком случае)
Если я где-то допустил ошибку — прошу поправить. Надеюсь это будет кому-то полезно.
Благодарю за внимание!
Комментарии: 7
Ты молодец! Но все, кто не хочет заморачиваться, используйте SendIt)))
В целом использовать AjaxForm в 2025 как-то не кошерно. Есть FetchIt, по бэку он может ничего нового не приносит, но хотя бы от jQuery не зависит.
Это само собой)) Я работал с тем что есть, и мне не хотелось все формы переписывать по новой, поэтому сделал так)
Можно было все не переписывать, а только одну, ту где загрузка файлов))) Но ты красавчик, что решил разобраться и поделиться!
Куда сохраняются файлы? Как поменять путь? Зачем проверять допустимое количество файлов в цикле оно же не меняется? Если имя файла будет содержать пробелы и кириллицу проблем не будет? А если загрузить файл, перезагрузить страницу и загрузить его повторно он сохраниться?
Артур, я в заголовке и написал — что это простая зона) Они ничего никуда не загружает) по сути это просто визуальная дроп-зона. При перезагрузке страницы всё очищается)
Зачем проверять допустимое количество файлов в цикле оно же не меняется?Ты прав, эту проверку можно вынести из цикла)
Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.