Air Datepicker, легкий і красивий вибір дати

Хочу поділитися з вами досвідом написання компонента вибору дати для текстового поля.



Результат роботи можна подивитися тут: Air Datepicker.

Введення
Працюючи над останнім проектом, виникла необхідність додати в додаток календар з можливістю вибрати конкретний місяць. Всі популярні плагіни таку можливість надають, мій вибір зупинився на Zebra Datepicker — маленький, функціональний, все здорово. Але деяких речей все ж не вистачало:

  1. передача об'єктів Date() параметри замість рядків
  2. менш громіздка розмітка
  3. гнучке позиціонування елемента
  4. анімація при появі
Скільки не доводилося працювати з датою, майже завжди у вихідних даних вона зберігалася в unix форматі, і для мене залишається загадкою, чому в багатьох плагінах при завданні, наприклад, мінімально можливої дати, потрібно передавати рядок: потрібно отримати дату, потім переробити її в рядок і вже потім передати плагіну, замість того, щоб просто віддати new Date(time).

Що стосується громіздкою розмітки, то до неї додається ще й таблична верстка, до клітинок якої без зайвих проблем не додати position: relative;.

Ну і наостанок все ж хочеться, щоб була можливість додати невелику анімацію, а з-за того, що багато популярні календарі використовують метод .show(), який задіює властивість display, плавні переходи (transition) додавати трудомістко.

Розробка
Календар я розділив на три частини:

// Основна частина
Datepicker

// Тіло календаря
Datepicker.Body

// Навігація
Datepicker.Navigation

При настанні будь-яких подій в тілі або навігації, вони повідомляють про це основної частини, і календар оновлює свій стан у відповідності з цими подіями.

У цій задачі мені допомогли getter's і setter's. Наприклад, при зміні місяці просто присвоюється нова відображається дата із зміненим номером місяця, і всередині геттера викликається метод перемальовування тіла і навігації календаря. Незважаючи на те, що можна було б обійтися і без них, мені даний підхід видається більш красивим. Наприклад, ось так виглядає метод переходу до наступного місяця, року або декаді, залежно від виду:

next: function() {
var d = this.parsedDate;

switch (this.view) {
case 'days':
this.date = new Date(d.year, d.month + 1, 1);
break;
case 'months':
this.date = new Date(d.year + 1, d.month, 1);
break;
case 'years':
this.date = new Date(d.year + 10, 0, 1);
break;
}
}

У свою чергу усередині геттера відбувається виклик відтворення елементів календаря (спрощено):

set date (val) {
this.currentDate = val;

this.currentView._render();
this.nav._render();
}

Точно так само відбувається перехід на інший вид, дуже просто:

this.view = 'months';


Формування розмітки
Основа для календаря виглядає наступним чином:

<div class="datepicker">
<i class="datepicker--pointer"></i>
<nav class="datepicker--nav"></nav>
<div class="datepicker--content"></div>
</div>

Без таблиць і натяку на них. Осередок є простим <div>… </div>, що дає можливість додавати псевдо елементи до них і позиціонувати контент всередині них як захочеться.

Я не бачу особливого сенсу в поділі на ряди клітинок, так як це додатковий непотрібний елемент. Всі дати йдуть один за одним, у них задана відносна ширина, яка дозволяє здійснювати перехід на інший рядок в потрібний момент.

Обчислення загальної кількості днів у місяці

Щоб сформувати коректний HTML, потрібно знати скільки днів у місяці. Для цього використовується невеликий трюк з передачею наступного місяця і нульовий дати (Date() дата місяця починається з одиниці).

Datepicker.getDaysCount = function (data) {
// Наприклад, нам потрібно дізнатися, скільки днів в грудні, передаємо наступний місяць, виходить січень.
// Але з-за того, що замість 1, ми передали 0, він вказує на останній день попереднього місяця,
// що в підсумку дає нам 31 число, або 31 день.
return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
};

Формування назв днів



Коли инициализируешь календар, можна задавати день, з якого починається тиждень. Мені здалося цікавим показати як можна сформувати розмітку з назвами днів з допомогою рекурсії:

/**
* @param firstDay - День, з якого починається тиждень
* @param [curDay] - Поточний день, для якого формується розмітка
* @param [html] - Весь html доступний на даний момент
* @param [i] - Поточний номер дня тижня
*/
_getDayNamesHtml: function (firstDay, curDay, html, i) {
curDay = curDay != undefined ? curDay : firstDay;
html = html ? html : ";
i = i != undefined ? i : 0;

// Якщо пройшли всі 7 днів, повертаємо готовий html
if (i > 7) return html;
// Якщо дійшли до останнього дня тижня, а загальний лічильник ще не більше 7, починаємо з першого дня тижня
if (curDay == 7) return this._getDayNamesHtml(firstDay, 0, html, ++i);

html += '<div class="datepicker--day-name' + (this.isWeekend(curDay) ? " -weekend-" : "") + '">' + this.localization.daysMin[curDay] + '</div>';

return this._getDayNamesHtml(firstDay, ++curDay, html, ++i);
},

Використання flexbox
Для позиціонування всередині календаря я використовую flexbox. Він з легкістю дозволяє відцентрувати контент всередині комірок, буде центром у всіх браузерах (які підтримують цю технологію) і на різних ОС, на відміну від техніки завдання висоти і такого ж міжрядкового інтервалу.

Плюс він дозволяє розташовувати елементи на рівновіддаленій відстані один від одного всього одним рядком:

.datepicker--nav {
justify-content: space between;
}

Не потрібно турбуватися про різних значеннях ширини, все буде розраховуватися автоматично.

Можна також згадати про кнопки «Сьогодні» і «Очистити»:



Якщо їх два, вони займають по 50% від всієї ширини, якщо одна, то вона займає всю ширину. Цього також можна досягти одним рядком:

.datepicker--button {
flex: 1;
}

Це означає, що елемент в разі необхідності може як збільшуватися, так і зменшуватися, але при цьому розміри всіх сусідів будуть однакові. Коли кнопка одна, вона розширюється на всю ширину, коли дві, вони пропорційно зменшуються і займають по 50%, і т. д. Можна додавати скільки завгодно елементів, у всіх них будуть однакові розміри, і в сумі вони будуть займати всю ширину батька.

В результаті ми отримуємо легкість позиціонування контенту як при використанні таблиць, але зберігаємо при цьому чистоту і валідність розмітки.

Позиціонування
Позиція елемента задається двома значеннями:

  1. сторона, з якої буде з'являтися календар
  2. положення на цій стороні
Якщо потрібно розташувати календар зверху праворуч, то значення буде виглядати як:

{
position: 'top left'
}

Для того, щоб додати анімацію «подъезжания» до текстового поля, я додав допоміжні класи, які говорять з якого боку треба починати анімацію. В даному випадку цей клас виглядав би як .-from-top-. За анімацію відповідають css transition і css transform. Це дозволяє досягти плавності, а також додавати кастомні переходи.

Що стосується Date()
Як я згадував на початку, мені не зовсім зрозумілі ситуації, коли замість об'єкта дати потрібно передавати рядок. Можливо це зручно при автоматичної ініціалізації, коли параметри потрібно передавати через data атрибути, але для мене все ж зручніше просто передати new Date(). Тим більше, що запис виду new Date(2015, 11, 17) не особливо складніше '2015-12-17'. Тому в мене по всіх параметрах, де визначається дата, необхідно передавати new Date().

Кілька слів про використання
Мені подобається практика автоматичної ініціалізації плагінів, тому для ініціалізації календаря до текстового поля досить додати клас 'datepicker-here' і все запрацює.

<input type="text" class="datepicker-here" data position="top right" data-min-view='months'/>

Опції можна передати через data атрибути.

Кастомизируемое вміст комірки
У Air Datepicker є можливість повністю змінювати вміст комірок. Це дозволяє додавати, наприклад, назви подій або якийсь допоміжний контент в осередку. Для цього потрібно використовувати опцію onRenderCell():

$('#datepicker').datepicker({
// Додамо свій контент у всі комірки з датою 31 грудня.
onRenderCell: function (date, cellType) {
if (cellType == 'day' && date.getDate() == 31 && date.getMonth() == 11) {
return {
classes: '-ny-',
html: 'Новий рік!'
}
}
}
})

Висновок
У підсумку я можу сказати, що отримав непоганий досвід, покращив свої навички роботи з датою і написання документації. Календар вийшов невеликим: 20kb (минифицированный js файл), але достатньо функціональним, принаймні для мене він свої завдання виконує. Буду радий, якщо він або ця стаття кому-небудь допоможе.

Дякую за увагу.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.