Website development
19 November 2013

PickMeUp — хороший jQuery datepicker plugin

Проблема


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

Демо Репозиторий на GitHub

Искренне надеюсь, кому-то кроме меня этот jQuery плагин пригодится, буду рад конструктивной критике и pull request-ам.

В ближайшем будущем можно встроить другие локализации, в репозитории bootstrap-datepicker их много разных.

UPD
Поддерживаем поднадоевшую традицию 30 строк.
https://gist.github.com/RubaXa/7547352, спасибо RubaXa

UPD
В версии 2.0.1 изменилась в лучшую сторону архитектура плагина.
По поводу пожеланий в комментариях:
В версии 2.1.0 добавились новые опции конфигурации + несколько исправлений.

+55
96.6k 307
Comments 52
Top of the day