ExtJs: легкий способ мультизагрузки и валидации файлов (используем HTML5 File API)

в 15:37, , рубрики: extjs 4, javascript, Библиотека ExtJS/Sencha, валидация, метки: ,

Если вам нужно валидировать загружаемые пользователем файлы не только на сервере, но и на клиенте, или вы хотите позволить пользователю выбрать сразу несколько файлов для загрузки (multiple=”multiple”) то, скорее всего, вы столкнетесь с некоторыми трудностями при использовании Ext.form.field.File.

Суть проблемы

Проверить расширение загружаемого файла, если пользователь выбрал только один файл, не составит труда — просто объявляем vtype и смотрим value input-а:

Ext.apply(Ext.form.field.VTypes, {
    file: function(val, field) {
        var types = ['rtf', 'pdf', 'doc'],
            ext = val.substring(val.lastIndexOf('.') + 1);
        if(Ext.Array.indexOf(types, ext) === -1) {
            return false;
        }
    }
    ,fileText: 'Invalid file'
});


Проблемы начинаются тогда, когда файлов несколько, и/или вам нужно проверить «вес» файла. Все дело в том, что в коде выше field ссылается не на input, в котором «лежат» выбранные файлы, а на table, в которой лежит input.
Достучаться до самого input-а достаточно легко:

var input = Ext.get(field.id+'-fileInputEl'); - и мы можем работать с input.files.

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

var input = document.getElementById(field.id+'-fileInputEl');

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

Если файл один, и вы не против использования не нативного input-а загрузки файлов, то проблема решена (можно сказать ее и не было) — получаем поле загрузки файла и с помощью File API валидируем вес файла. Иначе — некоторые проблемы остаются:

1. При выборе нескольких файлов (после некоторых манипуляций) поле на форме покажет вам только первый выбранный файл.
2. Если вы хотите нативный input — его атрибут size будет равен 1, после установки inputType: 'file' в конфигурации поля, и установка свойства size: 50 вам не поможет.

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

Хотя, если немного отвлечься от темы, когда ФФ в одном из недавних обновлений перенял Chrome-style поведение атрибута placeholder (текст перестал исчезать при фокусе на поле) — меня это слегка покоробило. Я не понимаю в чем плюсы такого поведения — всегда хочется стереть этот текст перед вводом.

Решение

Если погуглить, интернет, в большинстве случаев, посоветует вам валидировать файлы только на сервере, или использовать flash. Если хочется и можется — можно так и поступить. Еще я видел ux, который умеет грузить много файлов и валидировать их — тоже может быть вариантом.

А можно обойтись буквально несколькими строками, и использовать File API. При этом у нас пока отваливаются всякие IE, но лично я особых переживаний по этому поводу не испытываю — возможность загрузки все равно остается, а на сервере все равно валидировать. К тому же, если вы используете экст и пишите более-менее сложное приложение, то, скорее всего, вы можете диктовать браузеры конечному пользователю.

Поступим следующим образом — унаследуемся от textfield, выставим ему inputType: 'file', позволим устанавливать в конфигурации size input-а и атрибут multiple. Назначим всему этому делу alias для «короткого» доступа (xtype). Правила валидации опишем vtype-ом. Все это позволит использовать поле с нужным поведением много раз в любой вьюхе приложения, и гибко его конфигурировать (например, задавать разрешенные типы файлов и их максимальный размер).

Описываем поле:

    Ext.define('fileupload',{
        extend: 'Ext.form.field.Text'
        ,alias: 'widget.fileupload' // теперь мы можем написать xtype: 'fileupload'
        ,inputType: 'file'
        ,listeners: {
            render: function (me, eOpts) {
                var el = Ext.get(me.id+'-inputEl');
                el.set({
                    size: me.inputSize || 1
                });
                if(me.multiple) {
                    el.set({
                        multiple: 'multiple'
                    });
                }
            }
        }
    });

Тут даже не знаю, нуждается ли что-то в пояснениях. Если в двух словах — после «отрисовки» нативного input-а, для него устанавливается атрибут size (установленный при конфигурации или по дефолту — 1) и атрибут multiple, если он выставлен в конфигурации. Таким образом, теперь у нас есть нативный конфигурируемый input.

Описываем валидатор:

    Ext.apply(Ext.form.field.VTypes, {
        file: function(val, field) {
            var input, files, file
            ,acceptSize = field.acceptSize || 4096 //  Максимальный вес файлов
            ,acceptMimes = field.acceptMimes || ['rtf', 'pdf', 'doc', 'xls', 'xlsx', 'zip', 'rar']; // Разрешенные типы

            input = Ext.get(field.id+'-inputEl');
            files = input.getAttribute('files');
            if ( ! files || ! window.FileReader) {
                return true; // Мы имеем дело с неправильным браузером. Из вредности можно вернуть и false
            }
	
            for(var i = 0, l = files.length; i < l; i++) { // смотрим размеры файлов
                file = files[i];
                if(file.size > acceptSize * 1024) {
                    this.fileText = (file.size / 1048576).toFixed(1) + ' MB: invalid file size ('+(acceptSize / 1024).toFixed(1)+' MB max)';
                    return false;
                }

                var ext = file.name.substring(file.name.lastIndexOf('.') + 1);
                if(Ext.Array.indexOf(acceptMimes, ext) === -1) { // смотрим расширения файлов
                    this.fileText = 'Invalid file type ('+ext+')';
                    return false;
                }
            }
            return true;
        }
    });

Тут, думаю, тоже все достаточно понятно. Просто устанавливаем дефолтные типы/размер файлов, получаем input и пробегаемся по его files — проверяем их тип/размер. Все очень просто.

Теперь мы можем грузить и валидировать файлы, описывая поле формы следующим образом:

        {
            xtype: 'fileupload'
            ,vtype: 'file'
            ,multiple: true // разрешаем выбрать несколько файлов
            ,acceptMimes: ['doc', 'xls', 'xlsx', 'pdf', 'zip', 'rar'] // Устанавливаем разрешенные типы
            ,acceptSize: 2048 // Максимальный размер
            ,inputSize: 76 // Атрибут size
            ,fieldLabel: 'File <span class="gray">(doc, xls, xlsx, pdf, zip, rar; 2 MB max)</span>'
            ,msgTarget: 'under'
            ,name: 'filesToUpload[]'
        }

В действии все это хозяйство можно посмотреть тут — все собрано в один файл для наглядности, хотя вообще я люблю мвц 4го экста.

P.S.

Долго думал стоит ли мне тут что-то писать, хотя периодически возникает такое желание — хабр таки экспертная тусовка, а я не считаю себя js/extJs джедаем — просто по долгу службы пишу приложения которые работают. К тому же писатель из меня не очень, нету во мне креативности, а экст достоин куда более искусного рассказчика чем я. Да и вообще — экст на хабре мелькает достаточно редко, не знаю, есть ли тут к нему широкий интерес.
Потом дай, думаю, рискну. Авось — кому-то, да пригодится. Картинок нет специально, т.к. я считаю что картинки нужны тогда, когда нужно что-то изобразить, а не когда хочтеся привлечь внимание, а тут изображать ничего не требуется. Ну, если что не так — вы знаете что делать.

Автор: werdender

Источник

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


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