Pull to refresh

Собираем свой аналог Google Calendar не в 30 строк

Reading time 4 min
Views 22K

При разработке проекта для транспортной компании, занимающейся пассажирскими перевозками, возникла задача реализовать свой аналог Google Calendar для встраивания внутрь системы.
В силу некоторых причин(глубокая интеграция в проект, связь с кучей разных сущностей, полный контроль над всеми частями кода и прочее), использовать решение от Google было нерационально со многих точек зрения.

Итак, условия задачи:
  1. Интерфейс должен быть максимально приближен к интерфейсу от Google (т.к. до этого использовали его)
  2. Нормальная реализация RFC 2445, его части касательно RRULE (паттернов повторения)
  3. Быстрая скорость просчета дат событий (в данном случае рейсов) и их рендер в браузере
  4. Максимальное использование существующих библиотек для уменьшения потраченного времени .

Если тема интересна или вам есть что сказать, т.к. работа еще ведется и этот пост затрагивает лишь малую часть — прошу под кат, буду рад осмысленным советам.


Первоначально, перед детализацией, решено было определить общую стратегию работы данного решения. Попытка использовать ruby и gem ice_cube провалилась, т.к. уже довольно большое количество кода было написано на php и часть проекта выносить в другой язык программирования не очень кошерно. Ну и проблемы с производительностью (или моя неопытность работы с RoR).
Как итог, после размышлений, родилось следующее:
  1. За визуализацию в виде календаря будет ответственен немного доработанный jQuery плагин FullCalendar
  2. Создание повторяющихся рейсов возложено на Scheduler.js из набора FuelUX
  3. Хранение в БД паттернов повторения будет реализовано в виде «FREQ=DAILY;INTERVAL=2;UNTIL=20130130T230000Z;» для уменьшения размера БД (т.к. если хранить каждый рейс отдельно и окончание повторений не назначено количество отдельных рейсов стремится в бесконечность)
  4. Конвертация паттернов повторения в набор дат рейсов будет реализовано на стороне клиента для разгрузки серверных мощностей


Первое, что необходимо будет реализовать — получение RRULE, конвертация в набор дат и рендер с помощью плагина FullCalendar. После непродолжительного поиска было найдено следующее решение для конвертации — rrule.js работающее как в браузере, так и в качестве приложения на node.js, что в дальнейшем предоставляет возможность перенести на сервер с клиента.

Примерный путь ясен — приступим. Предупреждаю сразу, разрабатывается прототип и код пишется исходя из скоростных показателей, а не качественных.

Допустим у нас имеется json массив RRULE правил с полями имя, длина и сам паттерн повторений. Его получение с бекенда мы опустим.
[{
	"name": "Reccurence Event #1",
	"length": "120",
	"rrule": "DTSTART=20020201T083000Z;FREQ=WEEKLY;WKST=MO;BYDAY=WE,FR"
},
{
	"name": "Reccurence Event #2",
	"length": "120",
	"rrule": "FREQ=MONTHLY;DTSTART=20000201T083000Z;WKST=MO;BYDAY=TU"
},
{
	"name": "Reccurence Event #3",
	"length": "120",
	"rrule": "FREQ=DAILY;DTSTART=20000201T063000Z;WKST=MO;BYDAY=MO,FR"
}
]

Инициализируем массивы и создаем из строк RRULE объекты плагина rrule.js:
var data = private_env.get_data();			
var rules = new Array();
var occurs = new Array();
for (var k in data){
	rules.push(
		{
			name: data[k].name,
			length: data[k].length,
			rrule: RRule.fromString(data[k].rrule)
		});
}

Получаем из наших объектов список дат для каждого рейса, где DATE_START и DATE_END соответствуют началу и концу прмоежутка, за который нам необходимо их получить:
for (var k in rules){
	occurs.push(
		{
			name: rules[k].name,
			length: rules[k].length,
			occurs: rules[k]['rrule'].beetween(DATE_START,DATE_END)
		});

}

Очищаем календарь перед рендером:
$calendar.fullCalendar('removeEvents', function (event){
	return true;
});

И выводим наши рейсы на экран:
for (var k in occurs){
	for (var i in occurs[k].occurs){
		var event = {
			id: k,
			title: occurs[k].name,
			start: occurs[k].occurs[i],
			/* Не элегантно высчитываем конец рейса по длине в минутах, но пусть */
			end: new Date(occurs[k].occurs[i].getTime()+(1000*60*occurs[k].length)),
			allDay: false
			};
			/* Фикс renderEvent, чтобы не запускал рендер каждое добавление рейса, т.к. регрессия производительности */
			$calendar.fullCalendar('renderEvent', event, 1, 0);
	}
}
/* Рендерим весь стек рейсов, отвечает последняя 1 за это */
this.fullCalendar('renderEvent',{allDay: false}, 0, 1);

Все вышенаписанное мы оборачиваем в одну функцию, к примеру render(DATE_START,DATE_END) и вызываем при событии viewRender плагина FullCalendar:
...
viewRender: function(view, element){
	$(private_env.env_self).service('render', 'between', view.visStart, view.visEnd);                
}
...


В данный момент у нас получилась примерно следующая картина:


UPD: Пример

Превращать пост в простыни из кода не вижу смысла, думаю общая идея ясна. Внешний вид и прочее доработать труда не составляет.

Пару слов о создании и редактировании.


Большое количество событий в FullCalendar позволяет нам реализовать функционал редактирования. Необходимый функционал:
  1. При клике открывается форма редактирования рейса
  2. При перетаскивании, ресайзе выдаем запрос на варианты действий. Редактирование отдельно взятого рейса, редактирование всей цепочки повторений, редактирование будущих повторений

Задача тривиальна и сводится к тому, чтобы навешать функций на события eventClick, eventDrop, eventResize. Кстати, у двух последних имеется возможность отмены действий:
...
revertFunc();
...

Единственный нюанс. При выборе «Изменять только отдельный рейс» мы режем «RRULE FREQ=MONTHLY;DTSTART=20000201T083000Z;WKST=MO;BYDAY=TU» на три разных паттерна.
То что было до рейса этого, сам текущий рейс и то, что будет после. Решается это изменением опций в правиле DTSTART и UNTIL.

Что касается создания рейсов — Scheduler.js может выдавать на выход строку повторений согласно RFC 2445, что и необходимо нам:
...
$('#myScheduler').scheduler('value')
...

Дорабатывать данную солянку из плагинов еще предстоит довольно долго, но направление куда двигаться ясно.
И спасибо вам за внимание, если имеется критика или предложения по улучшению — прошу в студию и буду только рад.
Tags:
Hubs:
+10
Comments 8
Comments Comments 8

Articles