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

от автора


При разработке проекта для транспортной компании, занимающейся пассажирскими перевозками, возникла задача реализовать свой аналог 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);                 } ... 

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

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

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

Большое количество событий в 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') ... 

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

ссылка на оригинал статьи http://habrahabr.ru/post/210808/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *