Select / Multiselect без JS

    Я (и, как мне кажется, многие из вас) сталкивался не раз с несовместимостью селектов с дизайном сайта. Боль состоит в том, что их нельзя стилизовать, а в каждом браузере они выглядят по-своему.


    Конечно, есть огромное количество решений, представляемых фреймворками/библиотеками (тот же бутстрап). Но все они предполагают наличие JSа. Разумеется, в этом нет ничего страшного/плохого, но я попробовал сделать стилизуемый селект без JS в качестве фоллбэка на случай, если js по каким-либо причинам сломается.


    Select


    Выбор инструментов


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


        <div class="options">
            <label>
                <input type="radio" name="r" value="111" checked>
                <div class="value">11text11</div>
            </label>
        ....

    Мы обернули радио-кнопки в <label/>, чтобы не городить ненужных IDшников и for-ов.


    Логика


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


    #dropdown .options :checked + .value{
        position: absolute;
        top: 0;
    }

    Также нам надо соблюсти правило: высота каждого пункта должна быть равной "вакантному" месту (отступу сверху) для выбранного пункта


    #dropdown .options .value{
        height: var(--item-height);
    }
    #dropdown .options{
       padding-top: var(--item-height);
    }

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


    На ум приходит манипуляция псевдоселектором :focus. Добавим какой-нибудь инпут, который будет под него попадать. Я выбрал [type=text] потому, что ему можно задать размер (растянуть на всю ширину и высоту) и заслонить им лидирующий выбранный элемент.


    <div class="dropdown" id="dropdown">
        <input type="text">
        <div class="options">
        ....

    Скрывать выпадающий список будем ограничением высоты и overflow: hidden:


    #dropdown .options{
        padding-top: var(--item-height);
        overflow: hidden;
        height: 0;
    }
    #dropdown > :focus + .options{
        height: var(--list-height);
    }

    Разумеется, инпута не должно быть видно (как и радио-кнопок, представляющих опции):


    #dropdown input{
        opacity: 0;
    }

    Замечание!

    Следует использовать opacity: 0; вместо display: none; по причине того, что у срытых элементов (visually: hidden в том числе) не может быть состояния :focus.


    Грязный хак


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


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


    #dropdown .options{
        ...
        transition: 0s .1s height;
    }

    Готово, смотрите пример (я добавил немного стилей для красоты): https://jsfiddle.net/2k1pvbyt/


    Мультиселект


    Если мы пойдем дальше, то нам захочется сделать таким же образом (без JS) мультиселект. Не каждый интегратор jquery-плагинов такой сделает с JS (JQuery), а мы-то с вами ишь чего вздумали! Ну сказано — сделано, нельзя упасть лицом в грязь. Попробуем разобраться, возможно ли это. И, если нет, что в каком именно моменте нельзя обойтись без js.


    Что нам нужно изменить в предыдущем примере, чтобы наш селект стал мульти? Каждый пункт должен иметь возможность быть выбранным в не зависимости от других. Очевидно, нам надо сменить радио-кнопки на чекбоксы:


              <label data-value="111">
                    <input type="checkbox" required>
              </label>

    Да, это еще не все, и required там не случайно, с его помощью мы будем манипулировать нашим списком.


            <fieldset>
                <label data-value="111">
                    <input type="checkbox" required>
                </label>
            </fieldset>

    Если мы обернем это в <fieldset/>, то у нас появится возможность манипулировать псевдоселекторами :valid / :invalid.


    Заметка
    <fieldset/>, как и <form/> матчится на селектор :valid в том случае, если внутри него все поля также :valid

    Чтобы понять, что именно мы должны сделать, надо четко сформулировать задачу:


    Пусть выделенный пункт будет в начале списка. На одной строке с ним прочие выделенные пункты.

    Поместить в начало списка выбранный элемент несложно, мы зададим родителю display: flex и будем играться со значением order:


    #dropdown .options > fieldset:invalid{
        order: 2;
    }
    #dropdown .options{
        display: flex;
        flex-wrap: wrap;
    }
    #dropdown fieldset{
        flex-basis: 100%;
    }

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


    #dropdown .options > fieldset:valid{
        flex-basis: 10%;
        z-index: 3;
    }

    Вуаля! похоже, что работает.


    Заключение


    У нас не получилось выяснить, где (в рамках задачи) мы не можем обойтись без js.
    Возможно (точно), на более сложных примерах так и будет.


    Дублирую ссылки на примеры:



    Ну и для тех, кто не хочет писать эту "кучу разметки" руками, накидал скрипт, который сделает это за вас.



    Дополнения и прочие issue приветствуются, без сомнения!


    UPD


    Пришла в голову идея использовать управляющий инпут. Он пригодится, если мы отойдем от концепции nojs, будем использовать некий js-конструктор для инициализации.


    Среди внесенных изменений:


    • При фокусе инпут мы не скрываем, а смещаем ниже лидирующего пункта
    • Под это дело увеличиваем «вакантное место»
    • При инициализации вешаем обработчик на этот инпут, который на событие input генерирует css-строку, необходимую для фильтрации

    » Смотреть пример
    » Инструкция и код

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 19

      +3
      Пример на JS fiddle как то совсем плохо работает под хромом. Выбирает далеко не с первого раза и даже не со второго.
        –2
        Скорее всего, медленно нажимаешь мышкой.
        Поиграйся, настрой побольше задержку или шустрей тыкай пальцем.
        За время задержки ты должен успеть нажать на кнопку и поднять палец, так сказать, совершить полный цикл клика.
          +2
          Извините, у меня не сработало (Chrome, тачпад). Так и представил, что пишу пользователю:
          «Если не выбрался select — попытайтесь шустрей тыкать пальцем».
          Попытался несколько раз.
          Сама идея очень интересная, но нерабочая…
            –1
            Это proof-of-concept, настройте под ваши нужды.
            Вынес задержку в отдельную переменную, чтоб вам было проще.
            https://jsfiddle.net/2k1pvbyt/2/
            +1
            У меня в FF вообще не работает
              0
              Работает во всех браузерах (кроме servo, он так и не открыл страницу). В том числе на андроиде, на винде.
              Если не успеваете — поиграйтесь с задержкой.
          +4
          Троллейбус из буханки хлеба.png

          Во-первых, под последним firefox ни один пример не работает.
          Во-вторых, не могу проверить, у вас с клавиатуры список управляется?
          В-третьих, если и что-то вдруг сломалось в JS (что само по себе очень плохая и редкая ситуация), то обычно пользователю просто дают нестилизованный select. Без js уже мало какой из сайтов будет полноценно работать, так что кто его отключил — пусть «страдает» стандартным селектом)
            +1

            Нужно убрать инпут и сделать сам дропдаун фокусируемым: https://jsfiddle.net/crh70ux0/1/

              0

              Ну и высоту списка лучше ограничить размерами экрана, а не константой в пикселях: https://jsfiddle.net/crh70ux0/2/

                0
                Улучшать можно бесконечно, цель была — попробовать концепт.
                  0

                  Попробовать концепт можно и у себя локально. Вы статью с какой целью опубликовали?

                    0
                    Потому, что это интересно — сделать без JS то, что без него не делают. Показать, на что способен CSS.
                    По той же причине, по которой люди восхищаются статьями типа «вам не нужен js для этого» и подобными.
                      0

                      Ту и костыли не нужны с анимациями и фейковыми инпутами

              +1
              Комментарий немного в сторону. Каждый раз когда выходит новый фреймворк, сердце обливается кровью — кто ж под него сделает хороший multiselect с autocompletion'ом, да так чтобы поддерживал ajax, группировку опций, кастомные шаблоны для опций и вот это все, что было в ui-select.
                0
                У меня под последним хромом с тачпада не заработало.
                И еще из замечаний, почему в конструктор передается id а не сразу нода? Я вот идишниками уже пару лет не пользуюсь
                  +1
                  FF 49.0.2 не работает вообще:
                  image
                    0
                    Только что обновил FF, до 49.0.2, все равно работает.
                    Дай больше информации, изучу вопрос.
                    Мак/линух/винда?
                      0
                      винда
                      0
                      Почти заработало в servo (:

                    Only users with full accounts can post comments. Log in, please.