Pull to refresh

MODx — собственный ajax календарь событий/новостей без Ditto

Reading time 8 min
Views 10K
Задали мне тут намедни задачку: сделать календарь событий на ModX Evolution.
Движок этот я искренне люблю, за его небывалую гибкость и лично мне понятность.

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



Для выполнения задачи нам понадобятся:
  • Документ (контейнер) с новостями. Это должны быть обычные документы modx.
  • Javaскрипт библиотеки jQueryUI и qTip. Первая будет рисовать календарь и обеспечивать его работу, а вторая — всплывающие события.
  • Самописный сниппет eventsCalendar, который мы скоро вместе напишем.
  • 2 чанка (html оформления). Первый общий, где будет вызов скриптов и оформление календаря, и второй — оформление одного события во всплывающем окне.
  • Параметр TV для документов-событий. Создается в админке modx.


Для начала, создадим TV параметр для документов-событий. Идем в админке modx в Элементы->Управление элементами->Параметры (TV)->Новый параметр (TV)
Имя параметра: event_date
Заголовок: event_date
Тип ввода: Date
Шаблоны: любой, главное, чтобы он был выставлен шаблоном у событий.

Теперь, когда вы зайдете в событие, внизу появится этот параметр, при клике будет окно выбора даты, по которой мы и выведем это событие в календаре.

Далее, нам нужно написать сниппет, который будет выдавать массив событий для jQueryUI.datepicker. Вообще, этот плагин предназначен не для
вывода событий, а для выбора дат. Возможно, использовать его для другой функции не совсем верное решение, но есть несколько причин, по которым я его использую так:

Во-первых, jQueryUI у меня уже есть на сайте, так зачем подключать что-то еще?
Во-вторых, datepicker отлично настраивается, есть возможность указать диапазон выводимых дат, повешать событие на выбор даты, на смену месяца и тд.
Все это позволит создать симпатичный календарь событий с ajax запросами без изобретения собственного велосипеда.
Примерно так я думал.

На деле, оказалось, что этот подход несет в себе пару неудобств, но о них позже.

Сниппет eventsCalendar. Работа на стороне сервера.

Итак, пишем сниппет. С некоторых пор, я уже «hello world!» не могу вывести без написания класса, так что, заранее извините.

<?php
class eventsCalendar {

    var $id; // Номер документа-контейнера событий
    var $dateFormat = '%d %b %Y %H:%M'; // Формат даты, используется strftime()
    var $dateTV = 'event_date'; // название TV параметра modx для документа
    var $tplEvent = 'tplEvent'; // Шаблон оформления каждого события
    var $tplMain = 'tplCalendar'; // Общий шаблон оформления календаря событий
    var $conv = 0; // Нужна ли конвертация событий из cp1251 в utf8. По умолчанию - не нужна. 

/*	Вывод ошибок	*/
    function error($err) {
        $arr = array(
                     'no_id' => 'Вы забыли указать id каталога для выборки событий',
                     'no_action' => 'Не указан метод для обработки ajax запроса.',
                     'no_result' => 'В заданом контейнере нет документов.'
                    );
        return $arr[$err];
    }
    
/*	Вывод событий, принимает номер контейнера событий, месяц и год	*/
    function getEvents($id = '', $month = '', $year = '') {
        global $modx;

        if (empty($id)) {return $this->error('no_id');}
        if (empty($month)) {$month = date('m');}
        if (empty($year)) {$year = date('Y');}
        if (strlen($month) == 1) {$month = '0'.$month;}
        
        /*	Получаем документы из контейнера, нас интересует их id и дата	*/
        $tmp = $modx->getDocumentChildrenTVars($id, array('id',$this->dateTV));
        
        /*	Собираем массив в нужном формате для дальнейшей обработки	*/
        if (empty($tmp)) {return $this->error('no_result');}
        else {
            foreach ($tmp as $v) {
                $d = strftime('%Y-%m', strtotime($v[0]['value']));
                if ($d == $year.'-'.$month) {$ids[] = $v[1]['value'];}

                $dates_arr[$v[1]['value']] = $v[0]['value'];
            }         
        }

		/*	Мы получили массив нужных документов $ids, и массив дат этих документов, друг с другом они не связаны, пока
			Теперь уже достаем документы полностью. Сортировка по дате побликации		*/
        $arr = $modx->getDocuments($ids, 1, 0, "id,pagetitle,introtext", "", 'pub_date ASC, id', 'ASC');

		/*	Если подходящих документов не найдено - возвращаем закодированый пустой массив, чтобы
			вывести пустой календарь	*/
        if (empty($arr)) {return json_encode(array());}

		/*	Если документы есть - обрабатываем их свойства, оформление и запихиваем в массив	*/
        /*	Для начала достаем указаный шаблон оформления каждого события	*/
        $tpl = $modx->getChunk($this->tplEvent);
        
        $i = 1;
        foreach ($arr as $v) {
        	/*	определяем переменные документа для подстановки в шаблон	*/
            $did = $v['id'];
            $url = $modx->makeUrl($did);
            $date = strftime($this->dateFormat, strtotime($dates_arr[$did]));
            $date2 = strftime('%Y-%m-%d', strtotime($dates_arr[$did]));
            $desc = $v['introtext'];
            $title = $v['pagetitle'];    
            /*	Это для номера события, если событие не одно в дне	*/
            if (isset($date3) && $date3 != $date2) {$i = 1;}
            $date3 = $date2;
            
            /*	Стандартная замена плейсхолдеров на переменные в шаблонах modx	*/
            $placeholders = array('[+ec.date+]','[+ec.title+]','[+ec.url+]','[+ec.desc+]','[+ec.num+]');
            $values = array($date, $title, $url, $desc, $i);
            $text = str_replace($placeholders, $values, $tpl);
            
            /*	Забивание результата в массив, с возможной перекодировкой из cp1251.	*/
            if ($this->conv != 0) {$dates[$date2] .= iconv('cp1251', 'utf-8', $text);}
            else {$dates[$date2] .= $text;}
            
            $i++;
        }

        /*	Т.к. jqueryui.datepicker нужен массив 'дата','css class','текст события' - приводим наш результат к такому виду	*/
        foreach($dates as $k => $v) {
            $dates2[] = array($k, '', $v);
        }
		/*	Возвращаем данные в формате json	*/
       return json_encode($dates2); 
    }

	/*	Просто вывод основного шаблона на экран, по умолчанию	*/    
    function output($tpl) {
        global $modx;
        
        $tpl = $modx->getChunk($tpl);
        
        echo $tpl;
    }

}
/*	Конец класса, дальше обвязка, для его использования в modx	*/

$Cal = new eventsCalendar;

$Cal->id = $id;	// Указывать обязательно!
/*	Если заданы параметры при вызове сниппета - переопределяем стандартные	*/
if (!empty($dateTV)) {$Cal->dateTV = $dateTV;}
if (!empty($dateFormat)) {$Cal->dateFormat = $dateFormat;}
if (!empty($tplEvent)) {$Cal->tplEvent = $tplEvent;}
if (!empty($tplMain)) {$Cal->tplMain = $tplMain;}
if (!empty($conv)) {$Cal->conv = $conv;}

/*	Если идет запрос через ajax	- вызываем нужный метод и останавливаем работу	*/
if ($_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') {
        $action = $_POST['action'];
        if (!empty($action)) {
            switch($action) {
                case 'getEvents': echo $Cal->getEvents($Cal->id, $_POST['month'], $_POST['year']); break;
            }
        }
        else {
            echo $Cal->error('no_action');
        }
    die();
}
/*	Если обычный вызов - выводим календарь на экран	*/
else {
    $Cal->output($Cal->tplMain);
}
?>


Далее, идем в админке modx в сниппеты, создаем новый, обзываем его eventsCalendar и копируем внутрь вышенаписанное.
Мы создали новый сниппет и вызываем его в любом месте страницы примерно так:
[!eventsCalendar?
&id=`13`
&dateTV=`event_date`
&dateFormat=`%d %b %Y %H:%M`
&tplMain=`tplCalendar`
&tplEvent=`tplEvent`
!]

Параметры смотрите в конце топика.

Только пока он ничего не выведет, ибо мы не написали шаблоны.


Чанки. Работа на стороне клиента.

Начнем с мелкого — шаблон одного события tplEvent.
<div class='event'>
    <span class='num'><b>[+ec.num+].</b></span>
    <span class='date'>[+ec.date+]</span>
    <span class='link'><a href='[+ec.url+]' target='_blank'>[+ec.title+]</a></span>
    <br />
    <span class='notice'>[+ec.desc+]</span>
</div>
<br />


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

Нужно подключить библиотеки (следите за путями).
<script type='text/javascript' src='[(site_url)]inc/js/jquery-1.4.4.min.js'></script> 
<script type='text/javascript' src='[(site_url)]inc/js/jquery-ui-1.8.6.custom.min.js'></script>
<script type='text/javascript' src='[(site_url)]inc/js/jquery.ui.datepicker-ru.js'></script> <!-- Локализация datepicker -->
<script type='text/javascript' src='[(site_url)]inc/js/jquery.qtip.js'></script>


Теперь, основная магия.
<script type='text/javascript'>
$(document).ready(function() {
    class_enabled = 'enabled'; // Класс оформления даты, в которой есть события
    class_disabled = 'disabled'; // Нет событий
    element = '#Calendar'; // Элемент DOM в который грузится календарь
    url = '/[~[*id*]~]'; // Урл страницы для отправки ajax, по умолчнию - текущая

    dates = getEvents(); // Достаем массив событий через ajax
    
    Calendar(dates); // Запускаем календарь и отдаем ему массив с датами
    Qtip();	// Запускаем qTip, для всплывающих событий
});

// Функция вызова календаря на экран. 
function Calendar(dates) {
    $(element).datepicker({
        language: 'ru',
        inline: true,
        dateFormat: 'dd.mm.yy',
        // При смене месяца - получаем новый массив с датами. После этого, через 1 секунду запускаем qTip, 
        // чтобы он сделал вновь созданные элементы DOM всплывашками.
        onChangeMonthYear: function(year, month) {
            dates = getEvents(year, month);
            window.setTimeout(
                function() {
                    Qtip()
                },
                1000
            );
        },
        // Перед показом каждой даты - прогоняем ее по массиву событий, чтобы выставить свойства.
        // Свойств 3: вкл/выкл, класс оформления и текст, который вставляется в title элемента td.
        beforeShowDay: function(d) {
            var date = $.datepicker.formatDate('yy-mm-dd', d);
            for (i = 0, c = dates.length; i < c; i++) {
                if (date == dates[i][0]) {
                    return [true, class_enabled, dates[i][2]];
                }
            }
            return [false, class_disabled];
       },
       // Что делать при клике по дате. У меня - ничего не делать.
       onSelect: function() {return false;}
    });
    
}

// Запрос массива событий через ajax из нашего сниппета.
// Тут важный момент - СИНХРОННОСТЬ запроса, потому, что, пока не придут данные - календарь нельзя отрисовывать.
// Иначе он все время будет пуст, так как данные придут позже, чем он выведется на экран.
function getEvents(year, month) {
    $.ajaxSetup({async: false});
    $.post(url, {action: 'getEvents', month: month, year: year}, 
        function(data) {
            if (data == 'null') {data = '[]';}
            dates = jQuery.parseJSON(data);
            response = dates; 
        }        
    )
    return response;            
}

// Запуск всплыващих подсказок qTip.
// Все параметры можно поглядеть на сайте автора. Выводится текст из title элемента.
function Qtip() {
    $(element + ' .' + class_enabled).qtip({
        prerender: true,
        show: {when: {event: 'mouseover'}, effect: {length: 0}, solo: true},
        hide: {when: {event: 'unfocus'}, effect: {length: 0}},
        position: {
            corner: {
                target: 'center',
                tooltip: 'topLeft'
            }
        },
        style: {height: 100,width: 300}
    });
}
</script>

<div id='Calendar'></div> -- Элемент, куда будет грузиться календарь

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

Как это работает?

При первом открытии страницы нам выводится просто шаблон tplCalendar. После его загрузки стартует функция запроса массива дат.
Как только, придет массив — запускается календарь, который при открисовке даты назначает ей стиль и текст для всплывающей подсказки.
Затем запускается qTip и генерирует всплывающие подсказки.

При смене месяца — запрашивается новый массив дат и заново запускаются всплывашки (так как DOM изменился).

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

Теперь о недостатках. Их несколько.
  • Необходимость вызова qTip после каждой операции с календарем. Так как невозможно узнать (по крайней мере, я не придумал как), что календарь
    готов, приходится делать это по таймауту в 1сек. Если сервер тормозной, или событий ОЧЕНЬ много, секунды может не хватить. С другой стороны,
    таймаут можно поменять, да и событий создать столько, чтобы обычный сервер не успел их обработать за секунду, я не смог.
  • Так как текст для всплывающего окна пишется в атрибут title элемента td, приходится помнить о ковычках и их экранировании.

Недостатки можно было бы обойти написанием своего календаря, на php под это конкретную задачу, но я решил, что достоинства datepicker перевешивают недостатки его использования для вывода событий.

Приложение.

Скачать архив со сниппетом и шаблонами
Посмотреть в работе (версия для Revolution)

Параметры сниппета eventsCalendar:
&id
по умолчанию: нет, но обязателен
значение: [int]
описание: ID существующего документа-контейнера событий.

&dateFormat
по умолчанию: '%d %b %Y %H:%M'
значение: переменные strftime()
описание: Формат даты.

&dateTV
по умолчанию: event_date
значение: Имя существующего tv параметра
описание: название TV параметра modx для документа.

&tplEvent
по умолчанию: tplEvent
значение: Имя существующего чанка modx
описание: Шаблон оформления каждого события.

&tplMain
по умолчанию: tplCalendar
значение:
описание: Общий шаблон оформления календаря событий.

&conv
по умолчанию: 0
значение: [int]
описание: Нужна ли конвертация событий из cp1251 в utf8.


Плэйсхолдеры сниппета eventsCalendar:
[+ec.num+]
Номер события в дне. Актуально, только если у вас больше одного события в день. Для правильной нумерации необходимо,
чтобы параметр даты публикации документа совпадал с TV параметром даты события. Иначе, нумерация может быть неверной.

[+ec.date+]
Дата события.
[+ec.url+]
Ссылка на событие.
[+ec.title+]
Заголовок, берется из pagetitle документа modx.
[+ec.desc+]
Краткое описание, берется из introtext документа modx.


Ссылки на нужные сайты.


UPD.
Багрепорты засылать на bezumkin@yandex.ru, или оставлять в этой теме.

UPD.2
Готова вторая версия календаря, БЕЗ datepicker, и связанных с ним недостатков!
Tags:
Hubs:
+35
Comments 26
Comments Comments 26

Articles