Делаем красивый input[type=file] для адаптивного сайта

Уже немало копий front-end разработчиков было сломано об проблему стилизации поля ввода input[type=file]. Суть проблемы заключается в том, что в спецификации HTML нет строгих правил, устанавливающих, как же должен отображаться браузером этот элемент. Более того, для input[type=file] не предусмотрено атрибутов, которые позволили бы изменить его внешний вид, с помощью стилей CSS можно изменить лишь вид его границы и шрифт, а средствами JavaScript, из соображений безопасности, нельзя сымитировать клик по этому элементу, который вызвал бы системное окно для выбора файла. Но что же делать, когда заказчик хочет адаптивный сайт с красивыми стилизованными формами, в которых нельзя обойтись без этого поля ввода?

Способы решения проблемы стилизации поля

За то время, сколько существует эта проблема (а существует она очень долго), было найдено несколько способов ее решения. Всего их существует три:

Способ №1 (самый распространенный)

Убедить заказчика, что можно жить и со стандартным input[type=file].

Способ №2

Написать/использовать готовый загрузчик файлов на Flash/Java-апплете. Используется, например, на habrastorage.org/

Способ №3 (буден использован в статье)

Средствами CSS «замаскировать» стандартный input[type=file], сделать его полностью прозрачным и поместить на месте стилизованного фейкового поля, чтобы клик по последнему вызывал клик по стандартному, и, как следствие, открывал системное окно выбора файла.

И у второго, и у третьего способа, разумеется, есть свои минусы. Существенный недостаток Flash/Java-решения в том, что для его работы нужны соответствующие плагины, которых в браузере пользователя может не оказаться. Недостаток «маскировочного» решения же заключается в том, что для его реализации необходимо использовать хаки (про это речь пойдет ниже), а также потому, что оно бессмысленно без использования JavaScript (ведь нужно же как-то различать состояния «файл не выбран» и «файл выбран» для стилизованного фейкового поля, что на одном CSS сделать невозможно).

Схема велосипеда

Посмотрев несколько рабочих реализаций способа №3, мне захотелось изобрести свой велосипед, но не простой, а золотойадаптивный! Во всех решениях (как, например, здесь и здесь), которые мне встречались, стилизованное поле для выбора файла имеет фиксированную ширину, либо имеет вид простой одинарной кнопки (тоже с фиксированной шириной). Задание ширины же фейкового поля в процентах либо невозможно в принципе, либо может привести к тому, что данное поле будет работать в некоторых браузерах не совсем так, как хотелось бы.

Поэтому ключевой задачей было поставлено создание «резинового» input[type=file], который на экранах мобильных устройств представлял бы из себя простую кнопку для выбора файла (имя выбранного файла выводится на ней же), а на широких экранах выглядел бы как привычное для всех текстовое поле + кнопка, которое может тянуться на всю ширину окна:

d5c5a6e22d148120565fedfff688b853
Схематический вид элемента на мобильных устройствах
20a849dc29b70bb644201aad8dd81a12
Схематический вид элемента на десктопных устройствах

Дабы упростить себе жизнь, было решено отказаться от поддержки «любимца» всех верстальщиков — браузера IE 8. Да, сейчас многие меня упрекнут за это, но во-первых, этот браузер сейчас используется всего 4,8% пользователей (с тенденцией на понижение), что видно из статистики, а во-вторых, у меня не было и нет большой уверенности, что реализовать подобный input[type=file] вообще возможно для этого браузера.

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

<div class="file_upload">
        <div>Файл не выбран</div>
        <button>Выбрать</button>
        <input type="file">
    </div>

 

«Тяни, Пятачок!»

Перейдем к стилям. Чтобы у читателя не сложилось неверное впечатление, что каждое используемое в статье значение свойств CSS имеет огромную важность (так называемые «магические числа»), договоримся помечать те из них, которые можно смело изменять под свои нужды, комментарием

/* example */

Договорились? Отлично! Начнем стилизовать наше фейковое поле выбора файла с его «обертки» — div.file_upload:

.file_upload{
    position: relative;
    overflow: hidden;
    font-size: 1em;          /* example */
    height: 2em;             /* example */
    line-height: 2em         /* the same as height */
}

 

— свойство position задается для того, чтобы относительно div.file_upload можно было абсолютно позиционировать его дочерние элементы, а свойство overflow — для того, чтобы скрывать все то, что по каким-то причинам не влезет в нашу обертку (а такое найдется, но об этом позже). На широких экранах наши красивые поле и кнопка должны отображаться в одну строку, поэтому зададим для обоих float:

.file_upload > div, .file_upload > button{
    float: left;
    height: 100%;
    margin: 0
}

 

Зададим также какую-нибудь базовую ширину для обоих элементов в процентах:

.file_upload > div{
    width: 80%;             /* example */
    padding-left: 1em;      /* example */
    margin-right: -1em;     /* the same as padding-left, but negative */
    line-height: inherit
}
.file_upload > button{
    width: 20%              /* example */
}

 

Поскольку мы хотим, чтобы на мобильных устройствах текстовое поле скрывалось, и оставалась одна кнопка выбора файла, необходимо задать media query:

@media only screen and ( max-width: 500px ){  /* example */
    .file_upload > div{
        display: none
    }
    .file_upload > button{
        width: 100%
    }
}

 

Ну а теперь — самое веселое! Необходимо сделать стандартный input[type=file] полностью прозрачным, и растаращитьрастянуть его до размеров «обертки» div.file_upload. Для реализации последнего применим хак в виде абсолютного позиционирования и свойства CSS 3 transform, с помощью которого увеличим элемент, например, в 20 раз (да, это самое обычное «магическое число»):

.file_upload input[type=file]{
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    transform: scale(20);
    letter-spacing: 10em;     /* IE 9 fix */
    -ms-transform: scale(20); /* IE 9 fix */
    opacity: 0;
    cursor: pointer
}

 

Как видно из приведенного выше фрагмента CSS, для IE 9 потребовались дополнительные костыли. Это связано с тем, что данный браузер при клике на текстовое поле не вызывает системное окно выбора файла, а любезно предлагает «стереть» имя уже выбранного, что символизируется мигающим текстовым курсором. Поэтому для него дополнительно задается огромный интервал между буквами, что увеличивает кнопку элемента до размеров div.file_upload. Отмечу также, что z-index в данном случае не указывается, т.к. элемент идет последним «потомком» в выбранной с самого начала разметке.

На примере десктопного браузера FireFox, сейчас наше кастомизированное поле выбора файла для разных размеров окна выглядит так:

1e69609eea9e2a2a9f904b2aeaaec9f2
058c6f2ee522c1242e76b26e20b74004

Нужно больше стилей!

Разумеется, в таком примитивном виде поле выбора файла вряд ли кого-то устроит, поэтому добавим дополнительные стили, которые сделают кнопку выбора файла, скажем, фиолетовой, добавят тени и т.д. Не забудем также добавить свой стиль для кнопки, когда на нее наведен курсор, стиль для нажатой кнопки, а еще добавим стиль для всего элемента, когда на нем находится фокус (будет применяться при помощи JavaScript):

/* Making it beautiful */

.file_upload{
    border: 1px solid #ccc;
    border-radius: 3px;
    box-shadow: 0 0 5px rgba(0,0,0,0.1);
    transition: box-shadow 0.1s linear
}
.file_upload.focus{
    box-shadow: 0 0 5px rgba(0,30,255,0.4)
}
.file_upload > button{
    background: #7300df;
    transition: background 0.2s;
    border: 1px solid rgba(0,0,0,0.1);
    border-color: rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);
    border-radius: 2px;
    box-shadow: 0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 1px 2px rgba(0, 0, 0, 0.05);
    color: #fff;
    text-shadow: #6200bd 0 -1px 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis
}
.file_upload:hover > button{
    background: #6200bd;
    text-shadow: #5d00b3 0 -1px 0
}
.file_upload:active > button{
    background: #5d00b3;
    box-shadow: 0 0 3px rgba(0,0,0,0.3) inset
}

 

Теперь наше поле выбора файла выглядит так:

2869be93ba2e5730f928173bd56f156c
d96175184184781ba35957a496d01392

Нужно больше костылей!

Поскольку мы делаем полноценное поле для выбора файла, то нужно позаботиться о том, чтобы его можно было комфортно заполнять и с клавиатуры (сейчас же фокус вначале устанавливается на стилизованную кнопку, а затем — на скрытыйinput[type=file], что никак визуально не проявляется). Для этого, разумеется, используем JavaScript. Чтобы не писать много кода, я позволю себе использовать популярную библиотеку jQuery:

var wrapper = $( ".file_upload" ),
        inp = wrapper.find( "input" ),
        btn = wrapper.find( "button" ),
        lbl = wrapper.find( "div" );
    btn.focus(function(){
        inp.focus()
    });
    // Crutches for the :focus style:
    inp.focus(function(){
        wrapper.addClass( "focus" );
    }).blur(function(){
        wrapper.removeClass( "focus" );
    });

 

Поле ввода до сих пор оставалось «мертвым» — при выборе файла имя последнего нигде не отображалось. Пришло время исправить и это:

    var file_api = ( window.File && window.FileReader && window.FileList && window.Blob ) ? true : false;

    inp.change(function(){
        var file_name;
        if( file_api && inp[ 0 ].files[ 0 ] )
            file_name = inp[ 0 ].files[ 0 ].name;
        else
            file_name = inp.val().replace( "C:\\fakepath\\", '' );

        if( ! file_name.length )
            return;

        if( lbl.is( ":visible" ) ){
            lbl.text( file_name );
            btn.text( "Выбрать" );
        }else
            btn.text( file_name );
    }).change();

— если браузер поддерживает File API, то имя файла определяется с помощью него, в противном случае оно вырезается из значения скрытого input[type=file]. Для мобильных устройств, когда элемент представляет из себя одну кнопку, имя выбранного файла выводится на ней же.

Казалось бы, все, что требуется, уже написано. А вот фигушки! Если выбрать файл, используя «мобильное» поле, а затем увеличить размер окна и перевести элемент в «десктопный», то в текстовом поле так и останется «Файл не выбран» — нужно каждый раз обновлять элемент при изменении размеров окна:

$( window ).resize(function(){
    $( ".file_upload input" ).triggerHandler( "change" );
});

 

И что же мы получили в итоге?

Полученное стилизованное поле выбора файла было успешно протестировано в следующих браузерах:

  • FireFox 22.0 (Linux, Windows)
  • Opera 12.16 (Linux, Windows)
  • Internet Explorer 9
  • Chromium 27.0 (Linux)
  • Apple Safari (iOS 6.3.1)
  • Android browser (Android 2.3.6)
  • Android FireFox

Из плюсов рассмотренного в статье подхода можно выделить следующие основные:

  • Не используется Flash.
  • Элемент можно легко стилизовать средствами CSS, используя современные технологии адаптивного дизайна.
  • Поле можно заполнять и с клавиатуры.
  • В целом, данный подход относительно прост.

Из основных минусов:

  • Необходимость использования JavaScript.
  • Использование хаков CSS (кто знает, будет ли все это работать и в новых версиях браузеров).
  • Необходимость писать дополнительные костыли для поля с атрибутом multiple.
  • Некроссбраузерность — отсутствует поддержка IE 8, а также других старых браузеров, для которых нужно обязательно использовать «браузерные» свойства CSS.

Рабочий пример можно посмотреть на CodePen.