Проблема
Начиная работу над очередным сайтом понадобился datepicker. Самый известный такой datepicker — в jQuery UI, но так как jQuery UI в проекте не использовался — тянуть даже его часть не хотелось, принялся за поиски достойной альтернативы.
Требования следующие:
- Выбор даты, нескольких дат, интервала
- Простота настройки внешнего вида
- Желательно без каких-либо зависимостей кроме jQuery
Требования вполне логичные, ничего сверх естественного.
Каково было мое удивление, когда просмотрев десятка два плагинов я не нашел подходящего.
Для любопытных — сразу демо того, что получилось в результате.
Ближе всех к требованиям оказался DatePicker.
Но у него было несколько недостатков:
- Избыточная табличная верстка
- Старый стиль оформления картинками
- Просто старый, не развивался с 2009 года
- Несколько досадных багов
Решить их можно было кое-как разово, но было решено довести до ума, ведь не последний проект, да и другим может пригодиться.
Велосипед, так велосипед
Первое что захотелось изменить — убрать картинки для округления краев. Но картинки в ячейках таблицы. Убрал ячейки — отвалилась почти вся функциональность… Но ведь пути назад нет!
Потом пошел рефакторинг некоторых вещей, исправление багов, добавление мелких фич, снова исправление багов, и так по кругу.
На всё ушло около двух полных дней, и оно того стоило.
Что получилось
Как пример привожу код, генерируемый оригинальным DatePicker:
<div class="datepicker" id="datepicker_828" style="display: block; position: relative; width: 196px; height: 148px;">
<div class="datepickerBorderT"></div>
<div class="datepickerBorderB"></div>
<div class="datepickerBorderL"></div>
<div class="datepickerBorderR"></div>
<div class="datepickerBorderTL"></div>
<div class="datepickerBorderTR"></div>
<div class="datepickerBorderBL"></div>
<div class="datepickerBorderBR"></div>
<div class="datepickerContainer" style="width: 176px; height: 128px;">
<table cellspacing="0" cellpadding="0">
<tbody>
<tr>
<td>
<table cellspacing="0" cellpadding="0" class="datepickerViewYears">
<thead>
<tr>
<th class="datepickerGoPrev">
<a href="#">
<span>◀</span>
</a>
</th>
<th colspan="6" class="datepickerMonth">
<a href="#">
<span>2002 - 2013</span>
</a>
</th>
<th class="datepickerGoNext">
<a href="#">
<span>▶</span>
</a>
</th>
</tr>
<tr class="datepickerDoW">
<th>
<span>wk</span>
</th>
<th>
<span>Mo</span>
</th>
<th>
<span>Tu</span>
</th>
<th>
<span>We</span>
</th>
<th>
<span>Th</span>
</th>
<th>
<span>Fr</span>
</th>
<th>
<span>Sa</span>
</th>
<th>
<span>Su</span>
</th>
</tr>
</thead>
<tbody class="datepickerMonths">
<tr>
<td colspan="2">
<a href="#">
<span>Jan</span>
</a>
</td>
<td colspan="2">
<a href="#">
<span>Feb</span>
</a>
</td>
<td colspan="2">
<a href="#">
<span>Mar</span>
</a>
</td>
<td colspan="2">
<a href="#">
<span>Apr</span>
</a>
</td>
</tr>
<tr>
<td colspan="2">
<a href="#">
<span>May</span>
</a>
</td>
<td colspan="2">
<a href="#">
<span>Jun</span>
</a>
</td>
<td colspan="2">
<a href="#">
<span>Jul</span>
</a>
</td>
<td colspan="2">
<a href="#">
<span>Aug</span>
</a>
</td>
</tr>
<tr>
<td colspan="2">
<a href="#">
<span>Sep</span>
</a>
</td>
<td colspan="2">
<a href="#">
<span>Oct</span>
</a>
</td>
<td colspan="2">
<a href="#">
<span>Nov</span>
</a>
</td>
<td colspan="2">
<a href="#">
<span>Dec</span>
</a>
</td>
</tr>
</tbody>
<tbody class="datepickerDays">
<tr>
<th class="datepickerWeek">
<a href="#">
<span>27</span>
</a>
</th>
<td class="datepickerNotInMonth">
<a href="#">
<span>30</span>
</a>
</td>
<td class="">
<a href="#">
<span>1</span>
</a>
</td>
<td class="">
<a href="#">
<span>2</span>
</a>
</td>
<td class="">
<a href="#">
<span>3</span>
</a>
</td>
<td class="">
<a href="#">
<span>4</span>
</a>
</td>
<td class="datepickerSaturday">
<a href="#">
<span>5</span>
</a>
</td>
<td class="datepickerSunday">
<a href="#">
<span>6</span>
</a>
</td>
</tr>
<tr>
<th class="datepickerWeek">
<a href="#">
<span>28</span>
</a>
</th>
<td class="">
<a href="#">
<span>7</span>
</a>
</td>
<td class="">
<a href="#">
<span>8</span>
</a>
</td>
<td class="">
<a href="#">
<span>9</span>
</a>
</td>
<td class="">
<a href="#">
<span>10</span>
</a>
</td>
<td class="">
<a href="#">
<span>11</span>
</a>
</td>
<td class="datepickerSaturday">
<a href="#">
<span>12</span>
</a>
</td>
<td class="datepickerSunday">
<a href="#">
<span>13</span>
</a>
</td>
</tr>
<tr>
<th class="datepickerWeek">
<a href="#">
<span>29</span>
</a>
</th>
<td class="">
<a href="#">
<span>14</span>
</a>
</td>
<td class="">
<a href="#">
<span>15</span>
</a>
</td>
<td class="">
<a href="#">
<span>16</span>
</a>
</td>
<td class="">
<a href="#">
<span>17</span>
</a>
</td>
<td class="">
<a href="#">
<span>18</span>
</a>
</td>
<td class="datepickerSaturday">
<a href="#">
<span>19</span>
</a>
</td>
<td class="datepickerSunday">
<a href="#">
<span>20</span>
</a>
</td>
</tr>
<tr>
<th class="datepickerWeek">
<a href="#">
<span>30</span>
</a>
</th>
<td class="">
<a href="#">
<span>21</span>
</a>
</td>
<td class="">
<a href="#">
<span>22</span>
</a>
</td>
<td class="">
<a href="#">
<span>23</span>
</a>
</td>
<td class="">
<a href="#">
<span>24</span>
</a>
</td>
<td class="">
<a href="#">
<span>25</span>
</a>
</td>
<td class="datepickerSaturday">
<a href="#">
<span>26</span>
</a>
</td>
<td class="datepickerSunday">
<a href="#">
<span>27</span>
</a>
</td>
</tr>
<tr>
<th class="datepickerWeek">
<a href="#">
<span>31</span>
</a>
</th>
<td class="">
<a href="#">
<span>28</span>
</a>
</td>
<td class="">
<a href="#">
<span>29</span>
</a>
</td>
<td class="">
<a href="#">
<span>30</span>
</a>
</td>
<td class="datepickerSelected">
<a href="#">
<span>31</span>
</a>
</td>
<td class="datepickerNotInMonth">
<a href="#">
<span>1</span>
</a>
</td>
<td class="datepickerNotInMonth datepickerSaturday">
<a href="#">
<span>2</span>
</a>
</td>
<td class="datepickerNotInMonth datepickerSunday">
<a href="#">
<span>3</span>
</a>
</td>
</tr>
<tr>
<th class="datepickerWeek">
<a href="#">
<span>32</span>
</a>
</th>
<td class="datepickerNotInMonth">
<a href="#">
<span>4</span>
</a>
</td>
<td class="datepickerNotInMonth">
<a href="#">
<span>5</span>
</a>
</td>
<td class="datepickerNotInMonth">
<a href="#">
<span>6</span>
</a>
</td>
<td class="datepickerNotInMonth">
<a href="#">
<span>7</span>
</a>
</td>
<td class="datepickerNotInMonth">
<a href="#">
<span>8</span>
</a>
</td>
<td class="datepickerNotInMonth datepickerSaturday">
<a href="#">
<span>9</span>
</a>
</td>
<td class="datepickerNotInMonth datepickerSunday">
<a href="#">
<span>10</span>
</a>
</td>
</tr>
</tbody>
<tbody class="datepickerYears">
<tr>
<td colspan="2">
<a href="#">
<span>2002</span>
</a>
</td>
<td colspan="2">
<a href="#">
<span>2003</span>
</a>
</td>
<td colspan="2">
<a href="#">
<span>2004</span>
</a>
</td>
<td colspan="2">
<a href="#">
<span>2005</span>
</a>
</td>
</tr>
<tr>
<td colspan="2">
<a href="#">
<span>2006</span>
</a>
</td>
<td colspan="2">
<a href="#">
<span>2007</span>
</a>
</td>
<td colspan="2">
<a href="#">
<span>2008</span>
</a>
</td>
<td colspan="2">
<a href="#">
<span>2009</span>
</a>
</td>
</tr>
<tr>
<td colspan="2">
<a href="#">
<span>2010</span>
</a>
</td>
<td colspan="2">
<a href="#">
<span>2011</span>
</a>
</td>
<td colspan="2">
<a href="#">
<span>2012</span>
</a>
</td>
<td colspan="2">
<a href="#">
<span>2013</span>
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
</div>
и код, генерируемый PickMeUp:
<div class="pickmeup pmu-view-days" style="position: relative; display: inline-block;">
<div class="pmu-instance">
<nav>
<div class="pmu-prev pmu-button">◀</div>
<div class="pmu-month pmu-button">November, 2013</div>
<div class="pmu-next pmu-button">▶</div>
</nav>
<nav class="pmu-day-of-week">
<div>Mo</div>
<div>Tu</div>
<div>We</div>
<div>Th</div>
<div>Fr</div>
<div>Sa</div>
<div>Su</div>
</nav>
<div class="pmu-months">
<div class="pmu-button">Jan</div>
<div class="pmu-button">Feb</div>
<div class="pmu-button">Mar</div>
<div class="pmu-button">Apr</div>
<div class="pmu-button">May</div>
<div class="pmu-button">Jun</div>
<div class="pmu-button">Jul</div>
<div class="pmu-button">Aug</div>
<div class="pmu-button">Sep</div>
<div class="pmu-button">Oct</div>
<div class="pmu-button">Nov</div>
<div class="pmu-button">Dec</div>
</div>
<div class="pmu-days">
<div class="pmu-not-in-month pmu-button">28</div>
<div class="pmu-not-in-month pmu-button">29</div>
<div class="pmu-not-in-month pmu-button">30</div>
<div class="pmu-not-in-month pmu-button">31</div>
<div class=" pmu-button">1</div>
<div class="pmu-saturday pmu-button">2</div>
<div class="pmu-sunday pmu-button">3</div>
<div class=" pmu-button">4</div>
<div class=" pmu-button">5</div>
<div class=" pmu-button">6</div>
<div class=" pmu-button">7</div>
<div class=" pmu-button">8</div>
<div class="pmu-saturday pmu-button">9</div>
<div class="pmu-sunday pmu-button">10</div>
<div class=" pmu-button">11</div>
<div class=" pmu-button">12</div>
<div class=" pmu-button">13</div>
<div class=" pmu-button">14</div>
<div class=" pmu-button">15</div>
<div class="pmu-saturday pmu-button">16</div>
<div class="pmu-sunday pmu-button">17</div>
<div class="pmu-selected pmu-button">18</div>
<div class=" pmu-button">19</div>
<div class=" pmu-button">20</div>
<div class=" pmu-button">21</div>
<div class=" pmu-button">22</div>
<div class="pmu-saturday pmu-button">23</div>
<div class="pmu-sunday pmu-button">24</div>
<div class=" pmu-button">25</div>
<div class=" pmu-button">26</div>
<div class=" pmu-button">27</div>
<div class=" pmu-button">28</div>
<div class=" pmu-button">29</div>
<div class="pmu-saturday pmu-button">30</div>
<div class="pmu-not-in-month pmu-sunday pmu-button">1</div>
<div class="pmu-not-in-month pmu-button">2</div>
<div class="pmu-not-in-month pmu-button">3</div>
<div class="pmu-not-in-month pmu-button">4</div>
<div class="pmu-not-in-month pmu-button">5</div>
<div class="pmu-not-in-month pmu-button">6</div>
<div class="pmu-not-in-month pmu-saturday pmu-button">7</div>
<div class="pmu-not-in-month pmu-sunday pmu-button">8</div>
</div>
<div class="pmu-years">
<div class="pmu-button">2007</div>
<div class="pmu-button">2008</div>
<div class="pmu-button">2009</div>
<div class="pmu-button">2010</div>
<div class="pmu-button">2011</div>
<div class="pmu-button">2012</div>
<div class="pmu-button">2013</div>
<div class="pmu-button">2014</div>
<div class="pmu-button">2015</div>
<div class="pmu-button">2016</div>
<div class="pmu-button">2017</div>
<div class="pmu-button">2018</div>
</div>
</div>
</div>
Код не идеален, но намного проще и понятнее, а значит и оформлять его проще.
Что ещё нового по сравнению с оригиналом:
- Поддержка указания конфигурационных опций в data-атрибутах
- Глобальная конфигурация плагина
- Независимость языка каждого отдельного календаря
- Стили в маленьком scss файле с конфигурационными переменными — если нужно только изменить цвета — идеально подходит
- Макет резиновый, размер зависит от размера шрифта корневого элемента, выглядит одинаково хорошо при любом размере шрифта
- Размер плагина меньше оригинала
- Все классы имеют префикс
pmu-
, корневой элемент имеет классpickmeup
- Кое-какие мелочи
Так выглядит содержимое scss файла:
$border-radius : .4em;
$background : #000;
$color : #eee;
$color-hover : #88c5eb;
$nav-color : $color;
$nav-color-hover : $color-hover;
$not-in-month : #666;
$not-in-month-hover : #999;
$disabled : #333;
$selected-background : #136a9f;
$not-in-month-selected-background : #17384d;
$day-of-week : $not-in-month-hover;
@mixin display-flex() {
display : -ms-flexbox;
display : -webkit-flex;
display : flex;
}
.pickmeup {
background : $background;
border-radius : $border-radius;
display : none;
position : absolute;
* {
-moz-box-sizing : border-box;
box-sizing : border-box;
}
.pmu-instance {
display : inline-block;
height : 13.8em;
padding : .5em;
text-align : center;
width : 15em;
.pmu-button {
color : $color;
cursor : pointer;
outline : none;
text-decoration : none;
}
.pmu-button:hover {
color : $color-hover;
}
.pmu-not-in-month {
color : $not-in-month;
}
.pmu-disabled,
.pmu-disabled:hover {
color : $disabled;
cursor : default;
}
.pmu-selected {
background : $selected-background;
}
.pmu-not-in-month.pmu-selected {
background : $not-in-month-selected-background;
}
nav {
@include display-flex();
color : $nav-color;
line-height : 2em;
*:hover {
color : $nav-color-hover;
}
.pmu-prev,
.pmu-next {
height : 2em;
width : 1em;
}
.pmu-month {
width : 12em;
}
}
.pmu-years,
.pmu-months {
* {
display : inline-block;
line-height : 3.6em;
width : 3.5em;
}
}
.pmu-day-of-week {
color : $day-of-week;
cursor : default;
}
.pmu-day-of-week,
.pmu-days {
* {
display : inline-block;
line-height : 1.5em;
width : 2em;
}
}
.pmu-day-of-week * {
line-height : 1.8em;
}
}
&:not(.pmu-view-days) .pmu-days,
&:not(.pmu-view-days) .pmu-day-of-week,
&:not(.pmu-view-months) .pmu-months,
&:not(.pmu-view-years) .pmu-years {
display : none;
}
}
Итог
Размер минифицированного плагина:
* 14.8 KiB JavaScript (4.2 KiB gzip)
* 1.8 KiB CSS (650 B gzip)
C целью упрощения верстки поддерживается IE10+, и актуальные версии других браузеров (при желании можно сделать поддержку IE9-, но у меня такого желания нет, написал всё-таки в первую очередь для себя).
Искренне надеюсь, кому-то кроме меня этот jQuery плагин пригодится, буду рад конструктивной критике и pull request-ам.
В ближайшем будущем можно встроить другие локализации, в репозитории bootstrap-datepicker их много разных.
Автор: nazarpc