Pull to refresh

Select / Multiselect без JS

Reading time4 min
Views20K

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


Конечно, есть огромное количество решений, представляемых фреймворками/библиотеками (тот же бутстрап). Но все они предполагают наличие 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-строку, необходимую для фильтрации

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

Tags:
Hubs:
Total votes 28: ↑17 and ↓11+6
Comments19

Articles