Простая drag-n-drop зона для отправки файлов с помощью FormIt

Привет, друзья!

Передо мной возникла казалось бы, банальная задача — сделать форму, которая будет отправлять файлы на почту с 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 зоной и все они будут работать независимо друг от друга. Ну должны во всяком случае)

Если я где-то допустил ошибку — прошу поправить. Надеюсь это будет кому-то полезно.

Благодарю за внимание!
Дмитрий
22 марта 2025, 21:17
modx.pro
1
462
+6

Комментарии: 7

Артур Шевченко
22 марта 2025, 21:19
0
Ты молодец! Но все, кто не хочет заморачиваться, используйте SendIt)))
    Артур Шевченко
    22 марта 2025, 21:19
    0
    В целом использовать AjaxForm в 2025 как-то не кошерно. Есть FetchIt, по бэку он может ничего нового не приносит, но хотя бы от jQuery не зависит.
      Дмитрий
      22 марта 2025, 21:25
      0
      Это само собой)) Я работал с тем что есть, и мне не хотелось все формы переписывать по новой, поэтому сделал так)
        Артур Шевченко
        22 марта 2025, 21:29
        +1
        Можно было все не переписывать, а только одну, ту где загрузка файлов))) Но ты красавчик, что решил разобраться и поделиться!
          Артур Шевченко
          22 марта 2025, 21:38
          0
          Куда сохраняются файлы? Как поменять путь? Зачем проверять допустимое количество файлов в цикле оно же не меняется? Если имя файла будет содержать пробелы и кириллицу проблем не будет? А если загрузить файл, перезагрузить страницу и загрузить его повторно он сохраниться?
            Дмитрий
            22 марта 2025, 21:52
            0
            Артур, я в заголовке и написал — что это простая зона) Они ничего никуда не загружает) по сути это просто визуальная дроп-зона. При перезагрузке страницы всё очищается)
              Дмитрий
              22 марта 2025, 22:21
              0
              Зачем проверять допустимое количество файлов в цикле оно же не меняется?
              Ты прав, эту проверку можно вынести из цикла)
          Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
          7