Пользователь
0,0
рейтинг
9 августа 2013 в 15:50

Разработка → Делаем красивый input[type=file] для адаптивного сайта из песочницы tutorial

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

* — на момент написания этой статьи, мне было еще неизвестно, что уже во всех современных браузерах имитация клика по input[type=file] вызывает системное окно выбора файла. Большое спасибо lutov за ценный комментарий с ссылкой на рабочий пример от Pagefest!

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


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

Способ №1 (самый распространенный)
Убедить заказчика, что можно жить и со стандартным input[type=file].

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

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

Способ №4new! (будет рассмотрен в статье)
Поместить прозрачный input[type=file] внутрь элемента label, вместе с произвольными стилизованными инлайновыми элементами (кроме input, button, select и textarea, разумеется). Клик по label автоматически приведет к клику и по скрытому полю для выбора файла. Спасибо lampa за ценный комментарий!

Способ №5new! (будет рассмотрен в статье)
Использовать имитацию клика по скрытому input[type=file] средствами JavaScript. Да, это уже работает во всех современных браузерах. Еще раз спасибо lutov за ценный комментарий!
UPD: Внимание, данный способ неприменим для браузера Internet Explorer! Несмотря на то, что файл выбирается в скрытом input[type=file], при отправке формы значение последнего будет «сброшено». Спасибо LeonidFrolov за ценный комментарий!

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

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


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


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

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

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

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


Возможная верстка для способа с применением элемента label:
    <label class="file_upload">
        <span class="button">Выбрать</span>
        <mark>Файл не выбран</mark>
        <input type="file">
    </label>


Возможная верстка для решения на JavaScript (совпадает с версткой для «маскировочного» способа):
    <div class="file_upload">
        <button type="button">Выбрать</button>
        <div>Файл не выбран</div>
        <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: right, а для первого — небольшой внутренний отступ:

.file_upload > button{
    float: right;
    width: 8em;            /* example */
    height: 100%
}
.file_upload > div{
    padding-left: 1em      /* 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, сейчас наше кастомизированное поле выбора файла для разных размеров окна выглядит так:




«Все гениальное — просто!» или стили для способа с применением label


Основные стили, применяемые к текстовому полю и кнопке, для этого способа похожи на уже рассмотренные выше:

.file_upload{
    display: block;
    position: relative;
    overflow: hidden;
    font-size: 1em;              /* example */
    height: 2em;                 /* example */
    line-height: 2em             /* the same as height */
}
.file_upload .button, .file_upload > mark{
    display: block;
    cursor: pointer              /* example */
}
.file_upload .button{
    float: right;
    box-sizing: border-box;
    -moz-box-sizing: border-box;
    width: 8em;                  /* example */
    height: 100%;
    text-align: center           /* example */
}
.file_upload > mark{
    background: transparent;     /* example */
    padding-left: 1em            /* example */
}

Однако, теперь уже нет необходимости использовать хак с «растягиванием» прозрачного input[type=file]:

.file_upload input[type=file]{
    position: absolute;
    top: 0;
    opacity: 0
}


«Как это работает?» или стили для решения на JavaScript


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

.file_upload input[type=file]{
    position: absolute;
    top: 0;
    visibility: hidden
}


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


Разумеется, в таком примитивном виде поле выбора файла вряд ли кого-то устроит, поэтому добавим дополнительные стили, которые сделают кнопку выбора файла, скажем, фиолетовой, добавят тени и т.д. Не забудем также добавить свой стиль для кнопки, когда на нее наведен курсор, стиль для нажатой кнопки, а еще добавим стиль для всего элемента, когда на нем находится фокус (будет применяться при помощи 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
}


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




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


Поскольку мы делаем полноценное поле для выбора файла, то нужно позаботиться о том, чтобы его можно было комфортно заполнять и с клавиатуры (для «маскировочного» способа фокус сейчас вначале устанавливается на стилизованную кнопку, а затем — на скрытый 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" );
    });

Для метода с использованием label можно убрать участок кода, отвечающий за принудительный перенос фокуса с кнопки на input[type=file] (т.к. там у нас и не кнопка вовсе, а span).

Для метода, суть которого заключается в имитации клика по input[type=file], нужно, собственно, добавить эту самую имитацию:

    // Yep, it works!
    btn.add( lbl ).click(function(){
        inp.click();
    });
а также скорректировать код для установки класса .focus, предварительно убрав фрагмент, отвечающий за принудительный перенос фокуса:
    // Crutches for the :focus style:
    btn.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 для поддержки остальных «старичков».
  • Решение на JavaScript не поддерживается всеми версиями Internet Explorer, а также некоторыми старыми версиями других популярных браузеров (хотя ими уже практически никто и не пользуется).


Какой из трех двух элегантных способов создания стилизованного input[type=file] выбрать для повседневного использования — решать вам. Мой выбор — способ с использованием label (хоть он и имеет несемантичную верстку).

Рабочие примеры всех трех способов можно посмотреть на CodePen:

«Маскировочный» способ
Способ с использованием label
Способ с имитацией клика на JavaScript
Daniil Filippov @dif
карма
5,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (28)

  • –1
    Кажется, текст не по центру. Opera 12.16

    • –6
      Opera <=12 — это уже стало, как IE <=8, вообще-то.
      • +2
        Знаю людей, которые вообще не обновляют браузеры (в т.ч. и Оперу, поставленную им когда-то кем-то из знакомых) — и, скажу Вам по секрету, от этого своего браузеронеобновления хуже себя не чувствуют.

        А еще знаю людей, которые после 12-й ветки даже и не хотят на новую перебираться — да что там, здесь полно таких, кто выжидает, пока доделают в новой Опере то, что успешно (годами!) работало в той самой Opera «не выше 12».

        Я к тому, что обновление не самоцель (более того, существует обратная позиция, мол, защитой пусть у меня не браузер занимается, а антивирусный пакет, «за него для того и плочено»), а тем более обновление на, насомненно прогрессивную, но еще не дотягивающую по функционалу до прошлой ветки новую версию.

        А Вы всегда-всегда все обновляете до cutting-edge состояния? И Вас вот эти «Ваше ПО будет обновлено, через 2 минуты произойдет перезагрузка» (перезагрузка браузера или ОС — не суть важно, главное, что «предложение» поступает прямо во время работы) не отвлекают?
        • 0
          «Ваше ПО будет обновлено, через 2 минуты произойдет перезагрузка» — у меня такого нет, Firefox обновляется, когда запускается.
          • 0
            У Вас-то — да, но уверены ли Вы за всех? Простите, очень это самоуверенно с вашей стороны…

            Как простой вариант — системы самообновления стоят не так давно, до того браузеры спрашивали, «не обновиться ли», еще до того — просто работали :) Я легко себе представляю человека с Firefox этак третьей ветки — он и не парится про обновление )))

            А Вам не страшно автообновление оставлять включенным? )
            • 0
              А Вам не страшно автообновление оставлять включенным?

              А Вам страшно?
              • 0
                Вы знаете, я видел глазами, как автообновления укладывают машины до состояние незагружаемости (были такие обновления в карьере мелкософта). Я видел, как очередное обновление портов на Фряхе сносило модули из PHP (казалось бы, PHP — это не системный софт, автообновил — и живи). Я также видел, как после обновления переставали работать плагины в браузерах.

                Вы обновляетесь ради процесса, или ради результата? Я не против безопасности, но стабильность тоже имеет свою цену, как и ее отсутствие.
        • 0
          "… защитой пусть у меня не браузер занимается" — антивирусы сделаны для тех случаев, при которых вирус уже пробрался через дыру в браузере, а не для затыкания этих самых дыр.

          Использовать антивирус и браузер-решето — это как оставить дверь в квартиру открытой и всё время ходить с ножом.
    • 0
      Да, похоже, текст по вертикали находится чуть ниже, чем должен. Посмотрю, спасибо!

      К слову, тут уже нашлось кое-что посерьезней:

      image

      исправил код в статье и на CodePen.
  • +3
    via: Клик по input[type=«file»]

    <label class="uploadbutton">
        <div class="button" >Выбрать</div>
        <input type="file"/>
    </label>
    


    Пример начинающим велосипедостроителям: jsfiddle.net/tP8d9/5/
    • 0
      К сожалению, не работает в Opera 12.16 (Linux, Windows) и нативном Android Browser.

      Но это… гениально! :)
      • 0
        Да я сам узнал об этом способе только сегодня. Автор не я, поэтому кинул ссылку на хешкод. С оперой да, косяк. Но с оперой вечно какие-то косяки)
        • +1
          Замена для input[type=file]

          display: none

          на

          opacity: 0


          победила проблему Opera 12.16 и Android Browser. Ура, товарищи!

          fiddle.jshell.net/tP8d9/13/show/
    • +1
      скрывать input с помощью visibility и display нельзя — не будет работать в ие и опере
      рабочее решение с помощью clip:
      input {position: absolute; clip: rect(0,0,0,0)}
      
      • 0
        Спасибо, кастомные инпуты всем!)
      • 0
        А если так
        input {position: absolute; left:-9995px}
        • 0
          можно и так, но нужно учесть что при клике по лейблу браузер будет скролить страницу туда там где находится input, то бишь в вашем случае будет сброшен горизонтальный скролл в 0
  • 0
    В комментариях ко второй приведенной Вами статье прекрасный вариант, короткий, понятный и легко стилизуемый: habrahabr.ru/post/171743/#comment_5959055
    • 0
      А ведь действительно, работает! o_O

      Еще год назад с помощью имитации клика по input[type=file] было невозможно вызвать системное окно выбора файла: пример

      Да я и сам это проверял тоже около года назад, и получал отрицательный результат во всех браузерах, кроме IE (мне тогда понадобилось написать мультиаплоадер) — поэтому, перед написанием этой статьи, мне даже и не пришло в голову снова это перепроверить. А зря.

      В ближайшее время постараюсь исправить статью.
      • 0
        Имитация клика на JS работает во всех браузерах, но во всех версиях IE такая форма не будет сабмититься. Столкнулся с этой проблемой около года назад — тестировал верстку и все было окей, у инпута даже значение появлялось. Был очень рад, что нашел такое изящное решение. Разработчики сразу сказали, что с точки зрения безопасности что-то тут не так. При тестировании готового сайта в IE форма не отправлялась. С тех пор я за абсолютную псевдо-кнопку над прозрачным инпутом.
        • 0
          Мда… А счастье было так близко, эх :(

          Сделал для этого способа тестовую форму:

              <form enctype="multipart/form-data" action="test.php" method="POST">
                  <div class="file_upload">
                      <button type="button">Выбрать</button>
                      <div>Файл не выбран</div>
                      <input name="f" type="file">
                  </div>
                  <input type="submit" value="Отправить">
              </form>
          


          — действительно, файл не отправляется. Причина тому проста — IE при клике на кнопку input[type=submit] стирает имя выбранного файла в скрытом input[type=file] и переводит фокус на его текстовое поле:




          Спасибо за верное замечание, сейчас исправлю в статье.

          P. S. Извиняюсь также за то, что забыл задать в 1 и 3 примерах атрибут type="button" для кнопки — без него button работает как input[type=submit].
  • 0
    Флоаты я бы заменил на inline-block (сугубо личное убеждение), а для кнопки задал фиксированую ширину в пикселях (для ширины больше 500 разумееться)
    • 0
      Спасибо за замечание! Насчет inline-block, если честно, не понял, как это можно просто реализовать, зато значительно упростил свое первоначальное решение на флоатах, задав фиксированную ширину и float: right у кнопки.
      • 0
        С inline-block все тоже самое что и с флоатом, только пробелы нужно убрать. Почитайте в интернете, масса примеров.
        А в конктерном случае я бы сделал чуть иначе: для кнопки inline-block и фиксированиая ширина (например 150рх), а для поля с текстом block и отсруп margin-ом на соответствующую ширину
        • 0
          Примерно так? Мне не очень нравится это решение, потому что в нем для разных элементов фигурирует одно и то же значение (ширина кнопки) + обязательно нужно убрать пробельные символы между элементами в верстке.

          Впрочем, это дело вкуса.
          • 0
            Не совсем. Хотя я малость загнался, все равно флоат нужно использовать если ширина фиксированая. Я к тому, что мне комфортней когда ширина фиксированая, нежели зависит от процентов: логичней (для меня) когда поле для ввода меняет ширину, а не вместе с кнопкой. Но тут кому как удобней
            • 0
              зато значительно упростил свое первоначальное решение на флоатах, задав фиксированную ширину и float: right у кнопки.


              Так ведь давно уже переделал и исправил в тексте статьи :)
  • +1
    Переписал значительную часть текста статьи, добавил 2 новых элегантных способа стилизации input[type=file] (еще раз спасибо lampa и lutov за ссылки на рабочие примеры!).

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.