Недавно у меня была короткая работа по разработке ТЗ на модернизацию давно существующего проекта. И, в частности дело касалось стилизации пресловутых <input type="checkbox">. Выяснилось, что исполнитель, программист на все руки даже не понял, что я ему на словах объяснял как это сделать. Пришлось делать примеры и, как результат, появился этот текст.
Напомню, что сейчас checkbox и radiobox разные сайты изображают по-разному. Бывает, что не отмеченный input сразу и не разглядишь такой он дизайнерский красивый, а у последних версий Chrome выбранные checkbox стали гнусного цвета циан.
Итак, ситуация
Есть три компании, которые используют некий программный продукт, связанный с заказами, бухгалтерией, складом и пр. Работа с заказчиками, партнерами, исполнителями и пр.
Маркетологи и рекламщики тоже его используют. Что эта система делает неважно, на чем написано неважно.
А важно, что на сайте этого продукта есть много страниц с формами, на которых много input checkbox и radio.
Жалобы сотрудников
Директор: На большом экране плохо видно и незаметны крыжики.
Главбух: На моем компе крыжики выглядят так, у сотрудниц иначе, дома тоже не так, а на планшете совсем иначе.
Маркетолог: А можно, так что бы некоторые не выбранные позиции были красными, а другие выбранные были зелеными?
И т.д., и т.п.
Итак, задача
- Минимальными затратами и минимальными изменениями исправить внешний вид checkbox и radiobox.
- Сделать стилизацию checkbox и radiobox для разных юзеров. Важно: это закрытый сайт, там всё свои, красоты не нужны, а нужна эффективность восприятия.
Что нельзя
1. Серверную часть трогать нельзя.
2. Файлы javascript трогать нельзя, свой javascript вставлять нельзя.
3. Файлы css трогать нельзя.
А что можно
1. Править html шаблоны.
2. Создать файл стилей для всех юзеров.
4. Создать файл стилей для конкретного юзера или группы юзеров.
А что сделали можно сразу посмотреть на codepen.io, но лучше почитать дальше.
Предварительное изучение показало
1. Почти все <input type="checkbox"> имеют поле name, а которые не имеют, то у них есть id.
2. Все <input type="radio"> имеют поле name, некоторые имеют id.
3. Соответственно, в css к checkbox можно обращаться как по id, так и по name. К radio или по id, или по номеру потомка у родителя.
Фрагменты исходного кода:
/* вариант 1 */<tag><input type="checkbox"> Некий текст</tag>/* вариант 2 */<tag><input type="checkbox"> Некий текст<br><input type="checkbox"> Некий текст</tag>/* вариант 3 */...<label><input type="checkbox"> Некий текст</label>.../* вариант 4 */<td><input id="idxxx" type="checkbox"></td><td><label for="idxxx">Некий текст</label></td>
Так исправим код:
/* вариант 1 */<tag><label class="new-input"><input type="checkbox"><s></s><span>Некий текст</span></label></tag>/* вариант 2 */<tag><label class="new-input"><input type="checkbox"><s></s><span>Некий текст</span></label><br>...</tag>/* вариант 3 */...<label class="new-input"><input type="checkbox"><s></s><span>Некий текст</span></label>.../* вариант 4 */<td><label class="new-input new-input-one"><input id="idxxx" type="checkbox"><s></s></label></td><td><label for="idxxx">Некий текст</label></td>
Всё тоже самое и для <input type="radio">, класс у LABEL тот же.
Что конкретно сделали?
- Каждый input (корме варианта 3) обернули тэгом LABEL с нашим классом. Варианту 3 просто добавили класс.
- Сразу после input вставили пустой тэг S. Так как сам input будет не видим, то это тэг будет визуализировать это input.
- Сопроводительный текст обернули тэгом SPAN (кроме варианта 4). Этот тэг понадобиться, когда будем решать вопрос выравнивания визуального input относительно этого текста.
- Варианту 4 добавили еще класс, что бы не осуществлять это выравнивание, раз сопроводительный текст стоит в другой ячейки таблицы. Строго говоря, надо было бы сделать на оборот вариантам 1-3 добавить класс, отвечающий за выравнивание. Но, вариантов 1-3 гораздо больше, чем 4-го и что бы не раздувать html сделано так.
2. Почему тэги S и SPAN без классов? Ну, зачем раздувать html? Тем более, что не очевидно, что одна из конструкций ниже будет работать медленнее другой.
.new-input > S { } .new-input > .new-input-S {}
3. Как вы догадались, мне не нравятся идеи БЭМ, тем более идея раздувать html файл обилием упоминаний разных классов. В реальном проекте мы использовали только два класса mni и mnio. :-))
Некоторые предварительные рассуждения и настройки css касательно box-sizing:border-box, нормализации LABEL, селекторов A + B, A ~ B и [attr], псевдоклассов :checked, :disabled и ::before. Кто не уверен, что знает или хочет освежить знания смотрит под катом.
2. Проверка показала, что в нашем случае используется старая модель, а менять настройки страниц запрещено. Не наши LABEL это простые строчные элементы, в них только текст. Поэтому стилизуем ВСЕ LABEL.
LABEL { box-sizing:border-box; cursor:pointer; user-select:none;}LABEL *,LABEL *::before,LABEL *::after { box-sizing:inherit;}
Т.е., ставим box-sizing:border-box для тэга LABEL, всем его потомкам. Заодно ставим курсор и запрещаем выделение текст (что бы не мешало клику).
3. Комбинация селекторов A + B означает, что стили будут применяться только к селектору B, если он следует сразу ПОСЛЕ селектора A, т.е. только для первого B. С другой стороны, A ~ B означает, что ко всем селекторам B после селектора A, т.е. для первого и последующих.
Естественно, всё в пределах одного родителя.
Как это будем использовать?
<label class="new-input"><input type="checkbox"><s></s><span>Некий текст</span></label><label class="new-input"><input type="radio"><s></s><span>Некий текст</span></label>
/* 1 */.new-input > INPUT + S {}.new-input > INPUT ~ SPAN {}/* 2 */.new-input > INPUT:not(:checked) + S {}.new-input > INPUT:not(:checked) ~ SPAN {}/* 3 */.new-input > INPUT:checked + S {}.new-input > INPUT:checked ~ SPAN {}/* 4 */.new-input > INPUT:disabled + S {}.new-input > INPUT:disabled ~ SPAN {}/* 5 */.new-input > INPUT[type="radio"] + S {}
Первая группа общие стили для тэгов S и SPAN.
Вторая группа стили только когда INPUT НЕ выбран.
Третья стили только когда INPUT выбран.
Четвертая когда INPUT заблокирован.
И, наконец, пятая группа общие стили для тэга S ТОЛЬКО, если он стоит после input radio.
Таким образом, можно изменять стили тэгов S и SPAN в зависимости от состояния input.
4. Поскольку у нас тэг S будет изображать из себя input, то самому input поставим display:none, его не будет видно, а тэг LABEL будет его переключать, а тэг S будет соответственно меняться. Почему не используем html свойство hidden у input? Потому, что на некоторых браузерах hidden у input работает не совсем верно, плюс не будем перегружать html файл.
Итак, начинаем визуализацию input
Пример N 1. Самый простой используем алфавитные символы
html код тот же, а css будет такой:
/* s1 */.new-input > INPUT + S::before { content: "c";}/* s2 */.new-input > INPUT:checked + S::before { content: "V";}/* s3 */.new-input > INPUT[type="radio"] + S::before { content: "r";}/* s4 */.new-input > INPUT[type="radio"]:checked + S::before { content: "X";}/* s5 */.new-input > INPUT:disabled + S::before { opacity: 0.5;}/* s6 */.new-input > S { text-decoration: none; margin-left: 3px; margin-right: 6px;}/* s7 */.new-input > S::before { display: inline-block; width: 1.25em; text-align: center; color: #fafafa; background-color: #37474f;}/* s8 */.new-input > INPUT[type="radio"] + S::before { border-radius: 50%;}
Тэг S буде визуализировать input. Но мы разделим его по функционалу: сам тэг S будет отвечать за размещение в LABEL и выравнивание относительно следующего SPAN.
А псевдоэлемент S::before разместится внутри тэга S и будет изображать из себя input.
Строка s1 определяет, какой символ будет помещен в S::before когда input не выбран. В принципе надо было бы написать .new-input > INPUT:not(:checked) + S::before, но некоторые браузеры (например, IE), подобную конструкцию могут и не исполнить.
Строка s2 определяет символ, когда input выбран.
Строки s3 и s4 делают то же для input radio.
Строка s5 описывает, что будет если input заблокирован в данном случае тэг S будет наполовину прозрачным.
Строка s6 определяет выравнивание, в данном случае дает отбивку слева и справа (только в этом примере). Плюс, убирает штатное перечеркивание.
Строка s7 делает квадратик, s8 превращает его в кружок для input radio.
Пример N 1 можно посмотреть на codepen.io. Там представлены нативные input и новые. Первые можно убрать.
Для S::before указано display: inline-block в этом случае S::before внутри себя будет блоком (можно указать ширину, высоту, рамки и пр.), а снаружи он останется строчным элементом. В дальнейшем об этом будет подробнее.
Вопрос:
Может можно использовать специальные символы? Типа вот этих:
Задать им нужный размер и всё. Нет?
Ответ:
Можно. Но не нужно. Ибо будет большой геморрой и танцы с бубнами по заданию нужного размера, выравнивания по вертикали, обрезке по высоте и прочее. Плюс, разные браузеры с этими символами работают по-разному.
Мы пошли другим путем. Хотя в финальном примере есть реализация этой идеи.
Пример N 2. Рисуем элементы input средствами css
html код тот же, а css будет такой:
/* s1 */.new-input > S::before { content: ""; display: inline-block; width: 0.75em; height: 0.75em; border: 1px solid currentColor; padding: 2px; background-clip: content-box; border-radius: 20%;}/* s2 */.new-input > INPUT[type="radio"] + S::before { border-radius: 50%;}/* s3 */.new-input > INPUT:checked + S::before { background-color: currentColor;}/* s4 */.new-input > INPUT:disabled + S::before { opacity: 0.5;}/* s5 */.new-input > S { text-decoration: none; margin-left: 3px; margin-right: 6px;}
Строка s1 определяет S::before для визуализации input. Это будет inline-block, ширина и высота которого установлена в 0.75em, что примерно равно высоте прописной буквы и зависит от font-size родителя. Задана тонкая рамка текущим цветом, внутренняя отбивка, небольшое скругление углов. И самое важное! установлено свойство background-clip:content-box. Это очень интересное свойство если будет установлен background-color, то он закрасит только контентную часть и не затронет отбивку (padding). Что нам и надо.
Строка s2 для input типа radio делает S::before круглым.
Строка s3 для отмеченного input устанавливает для S::before background-color текущим цветом. Т.е., рисует внутри квадратик или кружок.
Строка s4 отрабатывает блокировку input, строка s5 дает отбивки слева и справа.
Преимущества этого метода
- Всё очень просто. Работает на всех браузерах. Даже у IE10 (в эмуляции у 11-го).
- Можно раскрашивать по своему усмотрению.
- Раз S::before это inline-block, то он сидит на
попебазовой линии ровно и никуда с нее не слезает. Если он по высоте будет больше текста, то просто увеличит высоту строки и останется на базовой линии. - Раз визуализация input находится внутри тэга S, то его можно легко позиционировать и выравнивать.
- Размеры S::before в em дают возможность задавать его размер относительно размера текста подписи. Можно, к примеру, поставить предельные значения высоты и ширины.
Недостатки этого метода
В основном в использовании размеров в em. Дело в том, что может возникнуть ситуация когда ширина и высота при расчете (из em в px) будет иметь дробное значение. На обычных компьютерах с обычным экраном округление может произойти не корректно. Например, размеры 12.8px на 12.8px у той же Мозилы могут стать как 13px на 12px. Тогда надо ставить фиксированные размеры. Хотя на современных мониторах и видеокартах, ноутбуках, на планшетах и смартфонах этого не происходит из-за того, что точка (пиксель) браузера состоит из нескольких пикселей экрана.
Пример N 2 можно посмотреть на codepen.io. Там представлены нативные input и новые. Первые можно убрать.
Итак, первую задачу визуализацию input выполнили. Переходим к избранной раскраске.
Раскрашиваем input
html для примера:
<label class="new-input"><input name="chb1" type="checkbox" ...><s></s><span>Некий текст</span></label><label class="new-input"><input id="rb1" type="radio" ...><s></s><span>Некий текст</span></label>
К input типа checkbox будем обращаться по name, к radio по id.
Всё красим в синий
/* только input */.new-input > INPUT[name="chb1"] + S,.new-input > INPUT#rb1 + S { color: #0091ea;}/* только text */.new-input > INPUT[name="chb1"] ~ SPAN,.new-input > INPUT#rb1 ~ SPAN { color: #0091ea;}/* или всё */.new-input > INPUT[name="chb1"] ~ *,.new-input > INPUT#rb1 ~ * { color: #0091ea;}
Помним о специфичности в css, эти стили будут более специфичны, чем базовые и сработают обязательно. Чем они отличаются от описанных выше? Тем, что применяются только к избранным input к тем, что имеет указанное значение name и id.
Тут всё хорошо кроме того, что не выбранные input будут не очень хорошо глядеться тонкая синяя рамка мало заметна.
Красим в зеленый, когда input выбран
/* только input */.new-input > INPUT[name="chb1"]:checked + S,.new-input > INPUT#rb1:checked + S { color: #00c853;}/* только text */.new-input > INPUT[name="chb1"]:checked ~ SPAN,.new-input > INPUT#rb1:checked ~ SPAN { color: #00c853;}/* или всё */.new-input > INPUT[name="chb1"]:checked ~ *,.new-input > INPUT#rb1:checked ~ * { color: #00c853;}
Первый вариант, на мой взгляд, не очень хорош зеленым будут и рамка, и внутренний квадратик/кружок. Можно раскрасить только его.
/* только input и только внутри */.new-input > INPUT[name="chb1"]:checked + S::before,.new-input > INPUT#rb1:checked + S::before { background-color: #00c853;}
Красим в красный, когда input НЕ выбран
/* только input */.new-input > INPUT[name="chb1"]:not(:checked) + S,.new-input > INPUT#rb1:not(:checked) + S { color: #d50000;}/* только text */.new-input > INPUT[name="chb1"]:not(:checked) ~ SPAN,.new-input > INPUT#rb1:not(:checked) ~ SPAN { color: #d50000;}/* или всё */.new-input > INPUT[name="chb1"]:not(:checked) ~ *,.new-input > INPUT#rb1:not(:checked) ~ * { color: #d50000;}
Логика понятна? Можно и дальше делать более сложные конструкции.
Например, при не выбранном input текст должен быть красным и жирным, а при выбранном внутренний элемент input и текст должен быть зеленым. Элементарно!
/* текст, когда нет выбора */.new-input > INPUT[name="chb1"]:not(:checked) ~ SPAN,.new-input > INPUT#rb1:not(:checked) ~ SPAN { color: #d50000; font-weight: bold;}/* внутренний элемент input, когда выбран */ .new-input > INPUT[name="chb1"]:checked + S::before,.new-input > INPUT#rb1:checked + S::before { background-color: #00c853;}/* текст, когда выбран */ .new-input > INPUT[name="chb1"]:checked ~ SPAN,.new-input > INPUT#rb1:checked ~ SPAN { color: #00c853;}
А, к примеру, надо обработать целую группу input (10-15 штук). Что бы не писать кучу строк можно найти их общего родителя (.parent_element) и сократить условие.
.parent_element > .new-input > INPUT:not(:checked) ~ SPAN { color: #d50000; font-weight: bold;}.parent_element > .new-input > INPUT:checked + S::before { background-color: #00c853;}.parent_element > .new-input > INPUT:checked ~ SPAN { color: #00c853;}
Всё можно посмотреть в финальном примере на codepen.io
Вот, вроде как, и всё. Осталось только почесать родимые пятна перфекциониста проблемы выравнивания.
Выравнивание визуального input и сопроводительного текста
Для начала напомню общеизвестные вещи на тему размещения текста, форматирования и прочего. Всё под катом.
1. Свойство font-size не определяет размер букв, а только размер знакоместа. Есть базовая линия (baseline), по которой расположены нормальные буквы. У ненормальных g ц нижние элементы свисают ниже её. Есть линия капители (cap height) это верхняя граница нормальной прописной (заглавной) буквы. У ненормальных Ё Й верхние элементы вылезают выше её. Иными словами, размер прописной буквы это расстояние от базовой линии до капители, а знакоместо это чуть больше сверху и снизу. Обычно в нормальных шрифтах высота капители это 75% от высоты знакоместо. К примеру, font-size:16px, а размер буквы Н у шрифта Arial будет 12px. Но, бывают специалисты у шрифтов которых всё не так.
2. Свойство line-height определяет высоту строки. Если его вычисленное значение больше, чем указано в font-size, то браузер разместит текст так, что бы нормальная прописная буква была по середине высоты строки. Есть нюансы, но тут они не важны.
3. Соответственно, в нашем случае тэги S и SPAN должны иметь одинаковые значения font-size и line-height желательно заданные где-то выше у родителей. В нашем случае в примерах font-size:16px и line-height:1.25. Поэтому в примере N1 у S::before ширина указана 1.25em, а высота у него определяется автоматически. А в примере N2 (и финальный пример) у S::before ширина и высота 0.75em, что бы был по высоте с прописную букву. Задав другое значение font-size ничего менять не надо. Естественно, эту величину надо подогнать под конкретный шрифт.
4. Если перед текстом стоит какая-то квадратная или круглая штучка, то любой дизайнер скажет, что она должна быть по высоте с прописную букву. А отбивка между ними должна быть в определенных процентах от размера шрифта. Если высота меньше высоты буквы, то она должна быть визуально значительно меньше, но не меньше 50%. Если больше, то тоже визуально значительно больше, но не больше 150%. А вот чуть-чуть, на пару пикселей больше/меньше это ужас-ужас! Ну, и расположена эта штучка должна быть на базовой линии или по середине без всяких там чуть-чуть.
Зачем я это упомянул? А затем, что перфекционисту глаза режет, когда input криво стоит рядом с текстом или прилипает, или далеко, или чуть меньше, или чуть больше. Мы так делать не должны!
Что будет, если сопроводительный текст в SPAN будет выведен в две или три строки? Очевидно, что он залезет под input. Это не красиво, надо исправить.
Один древний метод такой: тэгу S делаем float:left, а тэгу SPAN display:block и overflow:hidden.
Получится колонка текста. Подразумевается, что у кого-то из них будет соответствующий margin, что даст отбивку между ними. Ну, ещё добавляется геморрой с прекращением float после SPAN. Мы пойдем современным путем применим flexbox. Он тут совершенно к месту.
.new-input { display: flex; flex-direction: row; align-items: start;}.new-input > S { margin-right: 4px; flex: 0 0 auto;}.new-input > SPAN { flex: 0 1 auto;}
В этом случае тэг LABEL (который .new-input) будет flex, S и SPAN будут блоками, разместятся вверху LABEL. Текст в SPAN в случае необходимости будет в несколько строк. Вот из-за этого визуальный input описали в S::before. Независимо от высоты SPAN S::before будет расположен на одной базовой линии с первой строкой SPAN. Как вариант можно было указать align-items:center тогда при однострочном SPAN визуальный input был бы вверху, а при двух строках посередине, а при трех у второй строки. В финальном примере можно переключать расположение input.
Вот и всё
Надеюсь, было интересно и кому-нибудь полезно. Прошу, не сильно меня ругать это мой первый опыт на Хабр.
Пример N 1 просто демонстрация взаимодействия изменения input и соседнего элемента.
Пример N 2 визуализация input средствами css, как основа решения.
Финальный пример всё описанное вместе.
Про конкретную реализацию
Там были обширные формы, где блоки полей возможные для правки конкретным пользователям выделялись слабым фоном, а остальным input имели свойство disabled и служили только для информации. Поэтому стиль .new-input > INPUT:disabled + S::before не применяли.